From e43e27e2e19795797571f6ac8b27c8eff2bfed9d Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Thu, 3 Oct 2024 23:33:00 +0200 Subject: [PATCH] Models rewrite with new API for better typings and extensibility. --- package.json | 2 +- src/Model/Definition.ts | 34 --- src/Model/Model.ts | 408 +++++++++++++++++--------------- src/Model/Properties.ts | 10 + src/Model/PropertyDefinition.ts | 26 ++ src/Model/Types/ArrayType.ts | 31 +-- src/Model/Types/BoolType.ts | 28 ++- src/Model/Types/DateType.ts | 18 +- src/Model/Types/DecimalType.ts | 18 +- src/Model/Types/ModelType.ts | 47 ++-- src/Model/Types/NumericType.ts | 12 +- src/Model/Types/ObjectType.ts | 43 ++-- src/Model/Types/StringType.ts | 12 +- src/Model/Types/Type.ts | 16 +- src/Model/index.ts | 14 ++ src/index.ts | 18 +- tests/Model.test.ts | 79 ++----- 17 files changed, 430 insertions(+), 386 deletions(-) delete mode 100644 src/Model/Definition.ts create mode 100644 src/Model/Properties.ts create mode 100644 src/Model/PropertyDefinition.ts create mode 100644 src/Model/index.ts diff --git a/package.json b/package.json index fc41ebf..ccd6f30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sharkitek/core", - "version": "2.1.3", + "version": "3.0.0", "description": "Sharkitek core models library.", "keywords": [ "sharkitek", diff --git a/src/Model/Definition.ts b/src/Model/Definition.ts deleted file mode 100644 index 71daac9..0000000 --- a/src/Model/Definition.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {Type} from "./Types/Type"; - -/** - * Options of a definition. - */ -export interface DefinitionOptions -{ //TODO implement some options, like `mandatory`. -} - -/** - * A Sharkitek model property definition. - */ -export class Definition -{ - /** - * Initialize a property definition with the given type and options. - * @param type - The model property type. - * @param options - Property definition options. - */ - constructor( - public type: Type, - public options: DefinitionOptions = {}, - ) {} -} - -/** - * Initialize a property definition with the given type and options. - * @param type - The model property type. - * @param options - Property definition options. - */ -export function SDefine(type: Type, options: DefinitionOptions = {}): Definition -{ - return new Definition(type, options); -} diff --git a/src/Model/Model.ts b/src/Model/Model.ts index a63664a..f59a9f8 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,217 +1,231 @@ -import {Definition} from "./Definition"; +import {Definition} from "./PropertyDefinition"; /** - * Model properties definition type. + * Type definition of a model constructor. */ -export type ModelDefinition = Partial>>; -/** - * Model identifier type. - */ -export type ModelIdentifier = keyof T; +export type ConstructorOf = { new(): T; }; /** - * A Sharkitek model. + * Unknown property definition. */ -export abstract class Model +export type UnknownDefinition = Definition; + +/** + * A model shape. + */ +export type ModelShape = Record; + +/** + * Properties of a model based on its shape. + */ +export type PropertiesModel = { + [k in keyof Shape]: Shape[k]["_sharkitek"]; +}; + +/** + * Serialized object type based on model shape. + */ +export type SerializedModel = { + [k in keyof Shape]: Shape[k]["_serialized"]; +}; + +/** + * Type of a model object. + */ +export type Model = ModelDefinition & PropertiesModel; + +/** + * Identifier type. + */ +export type IdentifierType = Shape[K]["_sharkitek"]; + +/** + * Interface of a Sharkitek model definition. + */ +export interface ModelDefinition { - /** - * 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. - } - + getIdentifier(): IdentifierType; /** * 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". - + serialize(): SerializedModel; /** * Deserialize the model. + * @param obj Serialized object. */ - 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]); - }); + deserialize(obj: SerializedModel): Model; - // Reset original property values. - this.resetDiff(); + /** + * Find out if the model is new (never deserialized) or not. + */ + isNew(): boolean; + /** + * Find out if the model is dirty or not. + */ + isDirty(): boolean; - 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. - } + /** + * Serialize the difference between current model state and the original one. + */ + serializeDiff(): Partial>; + /** + * Set current properties values as original values. + */ + resetDiff(): void; + /** + * Get difference between original values and current ones, then reset it. + * Similar to call `serializeDiff()` then `resetDiff()`. + */ + save(): Partial>; +} + +/** + * Define a Sharkitek model. + * @param shape Model shape definition. + * @param identifier Identifier property name. + */ +export function model( + shape: Shape, + identifier?: Identifier, +): ConstructorOf>> +{ + // Get shape entries. + const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][]; + + return class GenericModel implements ModelDefinition> + { + constructor() + { + // Initialize properties to undefined. + Object.assign(this, + // Build empty properties model from shape entries. + Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel + ); + } + + /** + * Calling a function for each defined property. + * @param callback - The function to call. + * @protected + */ + protected forEachModelProperty(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType + { + for (const [propertyName, propertyDefinition] of shapeEntries) + { // For each property, checking that its type is defined and calling the callback with its type. + // 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; + } + } + + + /** + * The original properties values. + * @protected + */ + protected _originalProperties: Partial> = {}; + + /** + * The original (serialized) object. + * @protected + */ + protected _originalObject: SerializedModel|null = null; + + + + getIdentifier(): IdentifierType + { + return (this as PropertiesModel)?.[identifier]; + } + + serialize(): SerializedModel + { + // Creating an empty (=> partial) serialized object. + const serializedObject: Partial> = {}; + + this.forEachModelProperty((propertyName, propertyDefinition) => { + // For each defined model property, adding it to the serialized object. + serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel)?.[propertyName]); + }); + + return serializedObject as SerializedModel; // Returning the serialized object. + } + + deserialize(obj: SerializedModel): Model> + { + this.forEachModelProperty((propertyName, propertyDefinition) => { + // For each defined model property, assigning its deserialized value. + (this as PropertiesModel)[propertyName] = propertyDefinition.type.deserialize(obj[propertyName]); + }); + + // Reset original property values. + this.resetDiff(); + + this._originalObject = obj; // The model is not a new one, but loaded from a deserialized one. Storing it. + + return this as Model>; + } + + + isNew(): boolean + { + return !this._originalObject; + } + + isDirty(): boolean + { + return this.forEachModelProperty((propertyName, propertyDefinition) => ( + // For each property, checking if it is different. + propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel)[propertyName]) + // There is a difference, we should return false. + ? true + // There is no difference, returning nothing. + : undefined + )) === true; + } + + + serializeDiff(): Partial> + { + // Creating an empty (=> partial) serialized object. + const serializedObject: Partial> = {}; + + this.forEachModelProperty((propertyName, propertyDefinition) => { + // For each defined model property, adding it to the serialized object if it has changed or if it is the identifier. + if ( + identifier == propertyName || + propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel)[propertyName]) + ) // Adding the current property to the serialized object if it is the identifier or its value has changed. + serializedObject[propertyName] = propertyDefinition.type.serializeDiff((this as PropertiesModel)?.[propertyName]); + }); + + return serializedObject; // Returning the serialized object. + } + + resetDiff(): void + { + this.forEachModelProperty((propertyName, propertyDefinition) => { + // For each property, set its original value to its current property value. + this._originalProperties[propertyName] = (this as PropertiesModel)[propertyName]; + propertyDefinition.type.resetDiff((this as PropertiesModel)[propertyName]); + }); + } + + save(): Partial> + { + // Get the difference. + const diff = this.serializeDiff(); + + // Once the difference has been obtained, reset it. + this.resetDiff(); + + return diff; // Return the difference. + } + + } as unknown as ConstructorOf>>; } diff --git a/src/Model/Properties.ts b/src/Model/Properties.ts new file mode 100644 index 0000000..ccbe753 --- /dev/null +++ b/src/Model/Properties.ts @@ -0,0 +1,10 @@ +export {define} from "./PropertyDefinition"; + +export {array} from "./Types/ArrayType"; +export {bool, boolean} from "./Types/BoolType"; +export {date} from "./Types/DateType"; +export {decimal} from "./Types/DecimalType"; +export {model} from "./Types/ModelType"; +export {numeric} from "./Types/NumericType"; +export {object} from "./Types/ObjectType"; +export {string} from "./Types/StringType"; diff --git a/src/Model/PropertyDefinition.ts b/src/Model/PropertyDefinition.ts new file mode 100644 index 0000000..60b8fe8 --- /dev/null +++ b/src/Model/PropertyDefinition.ts @@ -0,0 +1,26 @@ +import {Type} from "./Types/Type"; + +/** + * Property definition class. + */ +export class Definition +{ + readonly _sharkitek: ModelType; + readonly _serialized: SerializedType; + + /** + * Create a property definer instance. + * @param type Property type. + */ + constructor(public readonly type: Type) + {} +} + +/** + * New definition of a property of the given type. + * @param type Type of the property to define. + */ +export function define(type: Type): Definition +{ + return new Definition(type); +} diff --git a/src/Model/Types/ArrayType.ts b/src/Model/Types/ArrayType.ts index 2dfe381..cd1d738 100644 --- a/src/Model/Types/ArrayType.ts +++ b/src/Model/Types/ArrayType.ts @@ -1,4 +1,5 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of an array of values. @@ -6,60 +7,60 @@ import {Type} from "./Type"; export class ArrayType extends Type { /** - * Constructs a new array type of a Sharkitek model property. - * @param valueType - Type of the array values. + * Initialize a new array type of a Sharkitek model property. + * @param valueDefinition Definition the array values. */ - constructor(protected valueType: Type) + constructor(protected valueDefinition: Definition) { super(); } - serialize(value: SharkitekValueType[]): SerializedValueType[] + serialize(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined { if (value === undefined) return undefined; if (value === null) return null; return value.map((value) => ( // Serializing each value of the array. - this.valueType.serialize(value) + this.valueDefinition.type.serialize(value) )); } - deserialize(value: SerializedValueType[]): SharkitekValueType[] + deserialize(value: SerializedValueType[]|null|undefined): SharkitekValueType[]|null|undefined { if (value === undefined) return undefined; if (value === null) return null; return value.map((serializedValue) => ( // Deserializing each value of the array. - this.valueType.deserialize(serializedValue) + this.valueDefinition.type.deserialize(serializedValue) )); } - serializeDiff(value: SharkitekValueType[]): any + serializeDiff(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined { if (value === undefined) return undefined; if (value === null) return null; // Serializing diff of all elements. - return value.map((value) => this.valueType.serializeDiff(value)); + return value.map((value) => this.valueDefinition.type.serializeDiff(value) as SerializedValueType); } - resetDiff(value: SharkitekValueType[]): void + resetDiff(value: SharkitekValueType[]|null|undefined): void { // Do nothing if it is not an array. if (!Array.isArray(value)) return; // Reset diff of all elements. - value.forEach((value) => this.valueType.resetDiff(value)); + value.forEach((value) => this.valueDefinition.type.resetDiff(value)); } } /** - * Type of an array of values. - * @param valueType - Type of the array values. + * New array property definition. + * @param valueDefinition Array values type definition. */ -export function SArray(valueType: Type) +export function array(valueDefinition: Definition): Definition { - return new ArrayType(valueType); + return define(new ArrayType(valueDefinition)); } diff --git a/src/Model/Types/BoolType.ts b/src/Model/Types/BoolType.ts index deaa8e4..8bba75a 100644 --- a/src/Model/Types/BoolType.ts +++ b/src/Model/Types/BoolType.ts @@ -1,22 +1,42 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of any boolean value. */ export class BoolType extends Type { - deserialize(value: boolean): boolean + deserialize(value: boolean|null|undefined): boolean|null|undefined { + // Keep NULL and undefined values. + if (value === undefined) return undefined; + if (value === null) return null; + return !!value; // ensure bool type. } - serialize(value: boolean): boolean + serialize(value: boolean|null|undefined): boolean|null|undefined { + // Keep NULL and undefined values. + if (value === undefined) return undefined; + if (value === null) return null; + return !!value; // ensure bool type. } } /** - * Type of any boolean value. + * New boolean property definition. */ -export const SBool = new BoolType(); +export function bool(): Definition +{ + return define(new BoolType()); +} +/** + * New boolean property definition. + * Alias of bool. + */ +export function boolean(): ReturnType +{ + return bool(); +} diff --git a/src/Model/Types/DateType.ts b/src/Model/Types/DateType.ts index 2337efb..f8cf1a7 100644 --- a/src/Model/Types/DateType.ts +++ b/src/Model/Types/DateType.ts @@ -1,22 +1,32 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of dates. */ export class DateType extends Type { - deserialize(value: string): Date + deserialize(value: string|null|undefined): Date|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + return new Date(value); } - serialize(value: Date): string + serialize(value: Date|null|undefined): string|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + return value?.toISOString(); } } /** - * Type of dates. + * New date property definition. */ -export const SDate = new DateType(); +export function date(): Definition +{ + return define(new DateType()); +} diff --git a/src/Model/Types/DecimalType.ts b/src/Model/Types/DecimalType.ts index aa199c5..4bb1438 100644 --- a/src/Model/Types/DecimalType.ts +++ b/src/Model/Types/DecimalType.ts @@ -1,22 +1,32 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of decimal numbers. */ export class DecimalType extends Type { - deserialize(value: string): number + deserialize(value: string|null|undefined): number|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + return parseFloat(value); } - serialize(value: number): string + serialize(value: number|null|undefined): string|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + return value?.toString(); } } /** - * Type of decimal numbers. + * New decimal property definition. */ -export const SDecimal = new DecimalType(); +export function decimal(): Definition +{ + return define(new DecimalType()); +} diff --git a/src/Model/Types/ModelType.ts b/src/Model/Types/ModelType.ts index 103bc84..b1641f6 100644 --- a/src/Model/Types/ModelType.ts +++ b/src/Model/Types/ModelType.ts @@ -1,44 +1,49 @@ import {Type} from "./Type"; -import {Model} from "../Model"; - -/** - * Type definition of the constructor of a specific type. - */ -export type ConstructorOf = { new(): T; } +import {define, Definition} from "../PropertyDefinition"; +import {ConstructorOf, Model, ModelShape, SerializedModel} from "../Model"; /** * Type of a Sharkitek model value. */ -export class ModelType> extends Type +export class ModelType extends Type, Model> { /** - * Constructs a new model type of a Sharkitek model property. - * @param modelConstructor - Constructor of the model. + * Initialize a new model type of a Sharkitek model property. + * @param modelConstructor Model constructor. */ - constructor(protected modelConstructor: ConstructorOf) + constructor(protected modelConstructor: ConstructorOf>) { super(); } - serialize(value: M|null): any + serialize(value: Model|null|undefined): SerializedModel|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + // Serializing the given model. - return value ? value.serialize() : null; + return value?.serialize(); } - deserialize(value: any): M|null + deserialize(value: SerializedModel|null|undefined): Model|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + // Deserializing the given object in the new model. - return value ? (new this.modelConstructor()).deserialize(value) : null; + return (new this.modelConstructor()).deserialize(value) as Model; } - serializeDiff(value: M): any + serializeDiff(value: Model|null|undefined): Partial>|null|undefined { + if (value === undefined) return undefined; + if (value === null) return null; + // Serializing the given model. - return value ? value.serializeDiff() : null; + return value?.serializeDiff(); } - resetDiff(value: M): void + resetDiff(value: Model|null|undefined): void { // Reset diff of the given model. value?.resetDiff(); @@ -46,10 +51,10 @@ export class ModelType> extends Type } /** - * Type of a Sharkitek model value. - * @param modelConstructor - Constructor of the model. + * New model property definition. + * @param modelConstructor Model constructor. */ -export function SModel>(modelConstructor: ConstructorOf) +export function model(modelConstructor: ConstructorOf>): Definition, Model> { - return new ModelType(modelConstructor); + return define(new ModelType(modelConstructor)); } diff --git a/src/Model/Types/NumericType.ts b/src/Model/Types/NumericType.ts index e4c249f..098965d 100644 --- a/src/Model/Types/NumericType.ts +++ b/src/Model/Types/NumericType.ts @@ -1,22 +1,26 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of any numeric value. */ export class NumericType extends Type { - deserialize(value: number): number + deserialize(value: number|null|undefined): number|null|undefined { return value; } - serialize(value: number): number + serialize(value: number|null|undefined): number|null|undefined { return value; } } /** - * Type of any numeric value. + * New numeric property definition. */ -export const SNumeric = new NumericType(); +export function numeric(): Definition +{ + return define(new NumericType()); +} diff --git a/src/Model/Types/ObjectType.ts b/src/Model/Types/ObjectType.ts index 61abc21..b2af673 100644 --- a/src/Model/Types/ObjectType.ts +++ b/src/Model/Types/ObjectType.ts @@ -1,66 +1,67 @@ import {Type} from "./Type"; -import {Definition} from "../Definition"; +import {define, Definition} from "../PropertyDefinition"; +import {ModelShape, PropertiesModel, SerializedModel, UnknownDefinition} from "../Model"; /** - * Type of a simple object. + * Type of a custom object. */ -export class ObjectType extends Type, Record> +export class ObjectType extends Type, PropertiesModel> { /** - * Constructs a new object type of a Sharkitek model property. - * @param fieldsTypes Object fields types. + * Initialize a new object type of a Sharkitek model property. + * @param shape */ - constructor(protected fieldsTypes: Record>) + constructor(protected readonly shape: Shape) { super(); } - deserialize(value: Record): Record + deserialize(value: SerializedModel|null|undefined): PropertiesModel|null|undefined { if (value === undefined) return undefined; if (value === null) return null; return Object.fromEntries( // For each defined field, deserialize its value according to its type. - (Object.entries(this.fieldsTypes) as [Keys, Definition][]).map(([fieldName, fieldDefinition]) => ( + (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => ( // Return an entry with the current field name and the deserialized value. [fieldName, fieldDefinition.type.deserialize(value?.[fieldName])] )) - ) as Record; + ) as PropertiesModel; } - serialize(value: Record): Record + serialize(value: PropertiesModel|null|undefined): SerializedModel|null|undefined { if (value === undefined) return undefined; if (value === null) return null; return Object.fromEntries( // For each defined field, serialize its value according to its type. - (Object.entries(this.fieldsTypes) as [Keys, Definition][]).map(([fieldName, fieldDefinition]) => ( + (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => ( // Return an entry with the current field name and the serialized value. [fieldName, fieldDefinition.type.serialize(value?.[fieldName])] )) - ) as Record; + ) as PropertiesModel; } - serializeDiff(value: Record): Record + serializeDiff(value: PropertiesModel|null|undefined): Partial>|null|undefined { if (value === undefined) return undefined; if (value === null) return null; return Object.fromEntries( // For each defined field, serialize its diff value according to its type. - (Object.entries(this.fieldsTypes) as [Keys, Definition][]).map(([fieldName, fieldDefinition]) => ( + (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => ( // Return an entry with the current field name and the serialized diff value. [fieldName, fieldDefinition.type.serializeDiff(value?.[fieldName])] )) - ) as Record; + ) as PropertiesModel; } - resetDiff(value: Record): void + resetDiff(value: PropertiesModel|null|undefined) { // For each field, reset its diff. - (Object.entries(this.fieldsTypes) as [Keys, Definition][]).forEach(([fieldName, fieldDefinition]) => { + (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).forEach(([fieldName, fieldDefinition]) => { // Reset diff of the current field. fieldDefinition.type.resetDiff(value?.[fieldName]); }); @@ -68,10 +69,10 @@ export class ObjectType extends Type(fieldsTypes: Record>): ObjectType +export function object(shape: Shape): Definition, PropertiesModel> { - return new ObjectType(fieldsTypes); + return define(new ObjectType(shape)); } diff --git a/src/Model/Types/StringType.ts b/src/Model/Types/StringType.ts index 9133ca4..b1b7267 100644 --- a/src/Model/Types/StringType.ts +++ b/src/Model/Types/StringType.ts @@ -1,22 +1,26 @@ import {Type} from "./Type"; +import {define, Definition} from "../PropertyDefinition"; /** * Type of any string value. */ export class StringType extends Type { - deserialize(value: string): string + deserialize(value: string|null|undefined): string|null|undefined { return value; } - serialize(value: string): string + serialize(value: string|null|undefined): string|null|undefined { return value; } } /** - * Type of any string value. + * New string property definition. */ -export const SString = new StringType(); +export function string(): Definition +{ + return define(new StringType()); +} diff --git a/src/Model/Types/Type.ts b/src/Model/Types/Type.ts index 8c983f6..d1682e4 100644 --- a/src/Model/Types/Type.ts +++ b/src/Model/Types/Type.ts @@ -1,25 +1,25 @@ /** * Abstract class of a Sharkitek model property type. */ -export abstract class Type +export abstract class Type { /** * Serialize the given value of a Sharkitek model property. - * @param value - Value to serialize. + * @param value Value to serialize. */ - abstract serialize(value: SharkitekType): SerializedType; + abstract serialize(value: ModelType|null|undefined): SerializedType|null|undefined; /** * Deserialize the given value of a serialized Sharkitek model. * @param value - Value to deserialize. */ - abstract deserialize(value: SerializedType): SharkitekType; + abstract deserialize(value: SerializedType|null|undefined): ModelType|null|undefined; /** * Serialize the given value only if it has changed. * @param value - Value to deserialize. */ - serializeDiff(value: SharkitekType): SerializedType|null + serializeDiff(value: ModelType|null|undefined): Partial|null|undefined { return this.serialize(value); // By default, nothing changes. } @@ -28,7 +28,7 @@ export abstract class Type * Reset the difference between the original value and the current one. * @param value - Value for which reset diff data. */ - resetDiff(value: SharkitekType): void + resetDiff(value: ModelType|null|undefined): void { // By default, nothing to do. } @@ -38,7 +38,7 @@ export abstract class Type * @param originalValue - Original property value. * @param currentValue - Current property value. */ - propertyHasChanged(originalValue: SharkitekType, currentValue: SharkitekType): boolean + propertyHasChanged(originalValue: ModelType|null|undefined, currentValue: ModelType|null|undefined): boolean { return originalValue != currentValue; } @@ -48,7 +48,7 @@ export abstract class Type * @param originalValue - Original serialized property value. * @param currentValue - Current serialized property value. */ - serializedPropertyHasChanged(originalValue: SerializedType, currentValue: SerializedType): boolean + serializedPropertyHasChanged(originalValue: SerializedType|null|undefined, currentValue: SerializedType|null|undefined): boolean { return originalValue != currentValue; } diff --git a/src/Model/index.ts b/src/Model/index.ts new file mode 100644 index 0000000..9fff0bb --- /dev/null +++ b/src/Model/index.ts @@ -0,0 +1,14 @@ +import * as property from "./Properties"; +export { property }; + +export * from "./Model"; +export {Definition} from "./PropertyDefinition"; + +export {ArrayType} from "./Types/ArrayType"; +export {BoolType} from "./Types/BoolType"; +export {DateType} from "./Types/DateType"; +export {DecimalType} from "./Types/DecimalType"; +export {ModelType} from "./Types/ModelType"; +export {NumericType} from "./Types/NumericType"; +export {ObjectType} from "./Types/ObjectType"; +export {StringType} from "./Types/StringType"; diff --git a/src/index.ts b/src/index.ts index db9c7c5..c1f92a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,4 @@ - -export * from "./Model/Model"; - -export * from "./Model/Definition"; - -export * from "./Model/Types/Type"; -export * from "./Model/Types/ArrayType"; -export * from "./Model/Types/BoolType"; -export * from "./Model/Types/DateType"; -export * from "./Model/Types/DecimalType"; -export * from "./Model/Types/ModelType"; -export * from "./Model/Types/NumericType"; -export * from "./Model/Types/ObjectType"; -export * from "./Model/Types/StringType"; +import * as s from "./Model"; +export * from "./Model"; +export { s }; +export default s; diff --git a/tests/Model.test.ts b/tests/Model.test.ts index e29da48..44980a1 100644 --- a/tests/Model.test.ts +++ b/tests/Model.test.ts @@ -1,39 +1,18 @@ -import { - SArray, - SDecimal, - SModel, - SNumeric, - SString, - SDate, - SBool, - Model, - ModelDefinition, - SDefine, ModelIdentifier -} from "../src"; -import {SObject} from "../src/Model/Types/ObjectType"; +import {s} from "../src"; /** * Another test model. */ -class Author extends Model +class Author extends s.model({ + name: s.property.string(), + firstName: s.property.string(), + email: s.property.string(), + createdAt: s.property.date(), + active: s.property.bool(), +}) { - name: string; - firstName: string; - email: string; - createdAt: Date; active: boolean = true; - protected SDefinition(): ModelDefinition - { - return { - name: SDefine(SString), - firstName: SDefine(SString), - email: SDefine(SString), - createdAt: SDefine(SDate), - active: SDefine(SBool), - }; - } - constructor(name: string = "", firstName: string = "", email: string = "", createdAt: Date = new Date()) { super(); @@ -48,7 +27,18 @@ class Author extends Model /** * A test model. */ -class Article extends Model
+class Article extends s.model({ + id: s.property.numeric(), + title: s.property.string(), + authors: s.property.array(s.property.model(Author)), + text: s.property.string(), + evaluation: s.property.decimal(), + tags: s.property.array( + s.property.object({ + name: s.property.string(), + }) + ), +}, "id") { id: number; title: string; @@ -58,27 +48,6 @@ class Article extends Model
tags: { name: string; }[]; - - protected SIdentifier(): ModelIdentifier
- { - return "id"; - } - - protected SDefinition(): ModelDefinition
- { - return { - id: SDefine(SNumeric), - title: SDefine(SString), - authors: SDefine(SArray(SModel(Author))), - text: SDefine(SString), - evaluation: SDefine(SDecimal), - tags: SDefine(SArray( - SObject({ - name: SDefine(SString), - }) - )), - }; - } } it("deserialize", () => { @@ -140,8 +109,8 @@ it("deserialize then save", () => { id: 1, title: "this is a test", authors: [ - { name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, }, - { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: new Date(), active: false, }, + { name: "DOE", firstName: "John", email: "test@test.test", createdAt: (new Date()).toISOString(), active: true, }, + { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, }, ], text: "this is a long test.", evaluation: "25.23", @@ -167,8 +136,8 @@ it("save with modified submodels", () => { id: 1, title: "this is a test", authors: [ - { name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, }, - { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: new Date(), active: false, }, + { name: "DOE", firstName: "John", email: "test@test.test", createdAt: (new Date()).toISOString(), active: true, }, + { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, }, ], text: "this is a long test.", evaluation: "25.23",