From f5502109ac0ae0c06c6f0406656d6f698b8b6c88 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sat, 29 Mar 2025 22:59:13 +0100 Subject: [PATCH] Rewrite model system, solve circular dependencies issues, better testing, clone function. --- jest.config.js | 10 - package.json | 24 +- src/Model/Model.ts | 296 ----------- src/Model/Properties.ts | 10 - src/Model/Types/DateType.ts | 37 -- src/Model/Types/ModelType.ts | 69 --- src/Model/Types/ObjectType.ts | 118 ----- src/Model/index.ts | 14 - src/index.ts | 4 - src/library.ts | 4 + src/model/index.ts | 16 + src/model/model.ts | 482 ++++++++++++++++++ src/model/properties.ts | 10 + .../property-definition.ts} | 12 +- .../ArrayType.ts => model/types/array.ts} | 32 +- .../BoolType.ts => model/types/boolean.ts} | 16 +- src/model/types/date.ts | 51 ++ .../DecimalType.ts => model/types/decimal.ts} | 4 +- src/model/types/model.ts | 116 +++++ .../NumericType.ts => model/types/numeric.ts} | 4 +- src/model/types/object.ts | 159 ++++++ .../StringType.ts => model/types/string.ts} | 4 +- .../Types/Type.ts => model/types/type.ts} | 35 +- src/utils.ts | 4 + tests/Model.test.ts | 168 ------ tests/model.test.ts | 329 ++++++++++++ tests/model/types/array.test.ts | 125 +++++ tests/model/types/boolean.test.ts | 53 ++ tests/model/types/date.test.ts | 62 +++ tests/model/types/decimal.test.ts | 47 ++ tests/model/types/model.test.ts | 107 ++++ tests/model/types/numeric.test.ts | 47 ++ tests/model/types/object.test.ts | 82 +++ tests/model/types/string.test.ts | 47 ++ vite.config.ts | 5 +- 35 files changed, 1825 insertions(+), 778 deletions(-) delete mode 100644 jest.config.js delete mode 100644 src/Model/Model.ts delete mode 100644 src/Model/Properties.ts delete mode 100644 src/Model/Types/DateType.ts delete mode 100644 src/Model/Types/ModelType.ts delete mode 100644 src/Model/Types/ObjectType.ts delete mode 100644 src/Model/index.ts delete mode 100644 src/index.ts create mode 100644 src/library.ts create mode 100644 src/model/index.ts create mode 100644 src/model/model.ts create mode 100644 src/model/properties.ts rename src/{Model/PropertyDefinition.ts => model/property-definition.ts} (72%) rename src/{Model/Types/ArrayType.ts => model/types/array.ts} (71%) rename src/{Model/Types/BoolType.ts => model/types/boolean.ts} (64%) create mode 100644 src/model/types/date.ts rename src/{Model/Types/DecimalType.ts => model/types/decimal.ts} (87%) create mode 100644 src/model/types/model.ts rename src/{Model/Types/NumericType.ts => model/types/numeric.ts} (82%) create mode 100644 src/model/types/object.ts rename src/{Model/Types/StringType.ts => model/types/string.ts} (82%) rename src/{Model/Types/Type.ts => model/types/type.ts} (50%) create mode 100644 src/utils.ts delete mode 100644 tests/Model.test.ts create mode 100644 tests/model.test.ts create mode 100644 tests/model/types/array.test.ts create mode 100644 tests/model/types/boolean.test.ts create mode 100644 tests/model/types/date.test.ts create mode 100644 tests/model/types/decimal.test.ts create mode 100644 tests/model/types/model.test.ts create mode 100644 tests/model/types/numeric.test.ts create mode 100644 tests/model/types/object.test.ts create mode 100644 tests/model/types/string.test.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 77a9c90..0000000 --- a/jest.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ - -export default { - preset: "ts-jest", - testEnvironment: "node", - - roots: [ - "./tests", - ], -}; diff --git a/package.json b/package.json index 8e1478d..0b1f8d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sharkitek/core", - "version": "3.3.0", + "version": "4.0.0", "description": "TypeScript library for well-designed model architectures.", "keywords": [ "deserialization", @@ -16,7 +16,7 @@ "repository": "https://code.zeptotech.net/Sharkitek/Core", "author": { "name": "Madeorsk", - "email": "madeorsk@protonmail.com" + "email": "m@deor.sk" }, "license": "MIT", "publishConfig": { @@ -24,24 +24,24 @@ }, "scripts": { "build": "tsc && vite build", - "test": "jest" + "test": "vitest", + "coverage": "vitest run --coverage" }, "type": "module", - "source": "src/index.ts", + "source": "src/library.ts", "types": "lib/index.d.ts", "main": "lib/index.js", "files": [ "lib/**/*" ], "devDependencies": { - "@types/jest": "^29.5.13", - "@types/node": "^22.7.4", - "jest": "^29.7.0", - "ts-jest": "^29.2.5", + "@types/node": "^22.13.14", + "@vitest/coverage-v8": "^3.0.9", "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "vite": "^5.4.8", - "vite-plugin-dts": "^4.2.2" + "typescript": "^5.8.2", + "vite": "^6.2.3", + "vite-plugin-dts": "^4.5.3", + "vitest": "^3.0.9" }, - "packageManager": "yarn@4.5.0" + "packageManager": "yarn@4.6.0" } diff --git a/src/Model/Model.ts b/src/Model/Model.ts deleted file mode 100644 index c9a3d2d..0000000 --- a/src/Model/Model.ts +++ /dev/null @@ -1,296 +0,0 @@ -import {Definition} from "./PropertyDefinition"; - -/** - * Type definition of a model constructor. - */ -export type ConstructorOf = { new(): T; }; - -/** - * Unknown property definition. - */ -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; - -/** - * Type of the extends function of model classes. - */ -export type ExtendsFunctionType>, Shape extends ModelShape, Identifier extends keyof Shape = any> = - (extension: ThisType & Extension) => ModelClass; - -/** - * Type of a model class. - */ -export type ModelClass>, Shape extends ModelShape, Identifier extends keyof Shape = any> = ( - ConstructorOf & { - extends: ExtendsFunctionType; - } -); - -/** - * Identifier type. - */ -export type IdentifierType = Shape[K]["_sharkitek"]; - -/** - * Identifier name type. - */ -export type IdentifierNameType = Shape extends ModelShape ? keyof Shape : unknown; - -/** - * Interface of a Sharkitek model definition. - */ -export interface ModelDefinition = Model> -{ - /** - * Get model identifier. - */ - getIdentifier(): IdentifierType; - - /** - * Get model identifier name. - */ - getIdentifierName(): IdentifierNameType; - - /** - * Serialize the model. - */ - serialize(): SerializedModel; - /** - * Deserialize the model. - * @param obj Serialized object. - */ - deserialize(obj: SerializedModel): ModelType; - - /** - * Find out if the model is new (never deserialized) or not. - */ - isNew(): boolean; - /** - * Find out if the model is dirty or not. - */ - isDirty(): boolean; - - /** - * 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()`. - */ - patch(): Partial>; -} - -/** - * Define a Sharkitek model. - * @param shape Model shape definition. - * @param identifier Identifier property name. - */ -export function model>, Shape extends ModelShape, Identifier extends IdentifierNameType = any>( - shape: Shape, - identifier?: Identifier, -): ModelClass -{ - // Get shape entries. - const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][]; - - return withExtends( - // Initialize generic model class. - class GenericModel implements ModelDefinition, ModelType> - { - 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]; - } - - getIdentifierName(): IdentifierNameType - { - return 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): ModelType - { - 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 unknown as ModelType; - } - - - 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] = structuredClone(this as PropertiesModel)[propertyName]; - propertyDefinition.type.resetDiff((this as PropertiesModel)[propertyName]); - }); - } - - patch(): 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 - ); -} - -/** - * Any Sharkitek model. - */ -export type AnyModel = Model; -/** - * Any Sharkitek model class. - */ -export type AnyModelClass = ModelClass; - -/** - * Add extends function to a model class. - * @param genericModel The model class on which to add the extends function. - */ -function withExtends>, Shape extends ModelShape, Identifier extends keyof Shape = any>( - genericModel: ConstructorOf -): ModelClass -{ - return Object.assign( - genericModel, - { // Extends function definition. - extends(extension: Extension): ModelClass - { - // Clone the model class and add extends function. - const classClone = withExtends(class extends (genericModel as AnyModelClass) {} as AnyModelClass as ConstructorOf); - // Add extension to the model class prototype. - Object.assign(classClone.prototype, extension); - return classClone; - } - } - ) as AnyModelClass as ModelClass; -} diff --git a/src/Model/Properties.ts b/src/Model/Properties.ts deleted file mode 100644 index ccbe753..0000000 --- a/src/Model/Properties.ts +++ /dev/null @@ -1,10 +0,0 @@ -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/Types/DateType.ts b/src/Model/Types/DateType.ts deleted file mode 100644 index b075c3c..0000000 --- a/src/Model/Types/DateType.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; - -/** - * Type of dates. - */ -export class DateType extends Type -{ - 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|null|undefined): string|null|undefined - { - if (value === undefined) return undefined; - if (value === null) return null; - - return value?.toISOString(); - } - - propertyHasChanged(originalValue: Date|null|undefined, currentValue: Date|null|undefined): boolean - { - return originalValue?.toISOString() != currentValue?.toISOString(); - } -} - -/** - * New date property definition. - */ -export function date(): Definition -{ - return define(new DateType()); -} diff --git a/src/Model/Types/ModelType.ts b/src/Model/Types/ModelType.ts deleted file mode 100644 index 9cfe1ba..0000000 --- a/src/Model/Types/ModelType.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; -import {ConstructorOf, Model, ModelShape, SerializedModel} from "../Model"; - -/** - * Type of a Sharkitek model value. - */ -export class ModelType extends Type, Model> -{ - /** - * Initialize a new model type of a Sharkitek model property. - * @param modelConstructor Model constructor. - */ - constructor(protected modelConstructor: ConstructorOf>) - { - super(); - } - - serialize(value: Model|null|undefined): SerializedModel|null|undefined - { - if (value === undefined) return undefined; - if (value === null) return null; - - // Serializing the given model. - return value?.serialize(); - } - - 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 (new this.modelConstructor()).deserialize(value) as Model; - } - - serializeDiff(value: Model|null|undefined): Partial>|null|undefined - { - if (value === undefined) return undefined; - if (value === null) return null; - - // Serializing the given model. - return value?.serializeDiff(); - } - - resetDiff(value: Model|null|undefined): void - { - // Reset diff of the given model. - value?.resetDiff(); - } - - propertyHasChanged(originalValue: Model|null|undefined, currentValue: Model|null|undefined): boolean - { - if (originalValue === undefined) return currentValue !== undefined; - if (originalValue === null) return currentValue !== null; - - // If the current value is dirty, property has changed. - return currentValue.isDirty(); - } -} - -/** - * New model property definition. - * @param modelConstructor Model constructor. - */ -export function model(modelConstructor: ConstructorOf>): Definition, Model> -{ - return define(new ModelType(modelConstructor)); -} diff --git a/src/Model/Types/ObjectType.ts b/src/Model/Types/ObjectType.ts deleted file mode 100644 index a036b17..0000000 --- a/src/Model/Types/ObjectType.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; -import {ModelShape, PropertiesModel, SerializedModel, UnknownDefinition} from "../Model"; - -/** - * Type of a custom object. - */ -export class ObjectType extends Type, PropertiesModel> -{ - /** - * Initialize a new object type of a Sharkitek model property. - * @param shape - */ - constructor(protected readonly shape: Shape) - { - super(); - } - - 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.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 PropertiesModel; - } - - 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.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 PropertiesModel; - } - - 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.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 PropertiesModel; - } - - resetDiff(value: PropertiesModel|null|undefined) - { - // For each field, reset its diff. - (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).forEach(([fieldName, fieldDefinition]) => { - // Reset diff of the current field. - fieldDefinition.type.resetDiff(value?.[fieldName]); - }); - } - - propertyHasChanged(originalValue: PropertiesModel|null|undefined, currentValue: PropertiesModel|null|undefined): boolean - { - // Get keys arrays. - const originalKeys = Object.keys(originalValue) as (keyof Shape)[]; - const currentKeys = Object.keys(currentValue) as (keyof Shape)[]; - - if (originalKeys.join(",") != currentKeys.join(",")) - // Keys have changed, objects are different. - return true; - - for (const key of originalKeys) - { // Check for any change for each value in the object. - if (this.shape[key].type.propertyHasChanged(originalValue[key], currentValue[key])) - // The value has changed, the object is different. - return true; - } - - return false; // No change detected. - } - - serializedPropertyHasChanged(originalValue: SerializedModel|null|undefined, currentValue: SerializedModel|null|undefined): boolean - { - // Get keys arrays. - const originalKeys = Object.keys(originalValue) as (keyof Shape)[]; - const currentKeys = Object.keys(currentValue) as (keyof Shape)[]; - - if (originalKeys.join(",") != currentKeys.join(",")) - // Keys have changed, objects are different. - return true; - - for (const key of originalKeys) - { // Check for any change for each value in the object. - if (this.shape[key].type.serializedPropertyHasChanged(originalValue[key], currentValue[key])) - // The value has changed, the object is different. - return true; - } - - return false; // No change detected. - } -} - -/** - * New object property definition. - * @param shape Shape of the object. - */ -export function object(shape: Shape): Definition, PropertiesModel> -{ - return define(new ObjectType(shape)); -} diff --git a/src/Model/index.ts b/src/Model/index.ts deleted file mode 100644 index 9fff0bb..0000000 --- a/src/Model/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index c1f92a4..0000000 --- a/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as s from "./Model"; -export * from "./Model"; -export { s }; -export default s; diff --git a/src/library.ts b/src/library.ts new file mode 100644 index 0000000..1553720 --- /dev/null +++ b/src/library.ts @@ -0,0 +1,4 @@ +import * as s from "./model"; +export * from "./model"; +export { s }; +export default s; diff --git a/src/model/index.ts b/src/model/index.ts new file mode 100644 index 0000000..b398e62 --- /dev/null +++ b/src/model/index.ts @@ -0,0 +1,16 @@ +import * as property from "./properties"; +export { property }; + +export * from "./model"; +export {Definition} from "./property-definition"; + +export {ArrayType} from "./types/array"; +export {BooleanType} from "./types/boolean"; +export {DateType} from "./types/date"; +export {DecimalType} from "./types/decimal"; +export {ModelType} from "./types/model"; +export {NumericType} from "./types/numeric"; +export {ObjectType} from "./types/object"; +export {StringType} from "./types/string"; + +export {circular} from "./types/model"; diff --git a/src/model/model.ts b/src/model/model.ts new file mode 100644 index 0000000..618ad48 --- /dev/null +++ b/src/model/model.ts @@ -0,0 +1,482 @@ +import {Definition, UnknownDefinition} from "./property-definition"; +import {ConstructorOf} from "../utils"; + +/** + * A model shape. + */ +export type ModelShape = Partial<{ + [k in keyof T]: Definition; +}>; + +/** + * Properties values of a model based on its shape. + */ +export type ModelPropertiesValues> = { + [k in keyof Shape]: Shape[k]["_sharkitek"]; +}; + +/** + * Serialized object type based on model shape. + */ +export type SerializedModel> = { + [k in keyof Shape]?: Shape[k]["_serialized"]; +}; + +/** + * This is an experimental serialized model type declaration. + * @deprecated + */ +type ExperimentalSerializedModel> + = Omit, ExperimentalSerializedModelOptionalKeys> + & Pick>, ExperimentalSerializedModelOptionalKeys>; +type ExperimentalSerializedModelBase> = { + [k in keyof Shape]: Shape[k]["_serialized"]; +}; +type ExperimentalSerializedModelOptionalKeys> = { + [k in keyof Shape]: Shape[k]["_serialized"] extends undefined ? k : never +}[keyof Shape]; + +/** + * A sharkitek model instance, with internal model state. + */ +export type ModelInstance, Identifier extends IdentifierDefinition> = T & { + /** + * The Sharkitek model state. + */ + _sharkitek: Model; +}; + +/** + * Identifier definition type. + */ +export type IdentifierDefinition> = (keyof Shape)|((keyof Shape)[]); + +/** + * Identifier type. + */ +export type IdentifierType, Identifier extends IdentifierDefinition> + = Identifier extends keyof Shape ? Shape[Identifier]["_sharkitek"] : { [K in keyof Identifier]: Identifier[K] extends keyof Shape ? Shape[Identifier[K]]["_sharkitek"] : unknown }; + +/** + * A model definition object. + */ +export interface ModelDefinition, Identifier extends IdentifierDefinition> +{ + /** + * Model class. + */ + Class: ConstructorOf; + + /** + * Properties names of the model identifier. + * Can be a single of a composite identifier. + */ + identifier?: Identifier; + + /** + * Model properties definition. + * Set properties types (serialized and deserialized). + */ + properties: Shape; +} + +/** + * A model property. + */ +export interface ModelProperty> +{ + /** + * Property name. + */ + name: keyof Shape; + + /** + * Property definition. + */ + definition: UnknownDefinition; + + /** + * Set if the property is part of the identifier or not. + */ + identifier: boolean; +} + +/** + * Model properties iterator object. + */ +export type ModelProperties> = ModelProperty[]; + +/** + * A Sharkitek model state. + */ +export class Model, Identifier extends IdentifierDefinition> +{ + /** + * The model manager instance. + */ + readonly manager: ModelManager; + + /** + * The model definition object. + */ + readonly definition: ModelDefinition; + + /** + * Iterable properties. + */ + readonly properties: ModelProperties; + + /** + * The actual instance of the model. + */ + instance: ModelInstance; + + /** + * Original values, to keep track of the changes on the model properties. + * @protected + */ + protected original: { + /** + * The original properties values. + */ + properties: Partial>; + + /** + * The original serialized object, if there is one. + */ + serialized: SerializedModel|null; + }; + + /** + * Initialize a new model state with the defined properties. + * @param manager The model manager. + */ + constructor(manager: ModelManager) + { + this.manager = manager; + this.definition = manager.definition; + this.properties = manager.properties; + } + + /** + * Initialize the Sharkitek model state for a new instance. + */ + initInstance(): this + { + return this.fromInstance( + // Initialize a new model instance. + new (this.definition.Class)() + ); + } + + /** + * Initialize the Sharkitek model state for the provided instance. + * @param instance The model instance. + */ + fromInstance(instance: T): this + { + // Initialize the sharkitek model instance. + const sharkitekInstance = instance as ModelInstance; + + // Keep the original instance, if it exists. + const originalInstance = sharkitekInstance._sharkitek; + + // Set references to instance / model state. + sharkitekInstance._sharkitek = this; + this.instance = sharkitekInstance; + + if (originalInstance) + // Share the same original values object. + this.original = originalInstance.original; + else + { // Initialize a new original values object, based on the current values of the instance. + this.original = { + properties: undefined, + serialized: null, + }; + this.resetDiff(); + } + + return this; + } + + /** + * Deserialize provided data to a new model instance. + * @param serialized Serialized model. + */ + deserialize(serialized: SerializedModel): this + { + // Initialize a new model instance. + this.initInstance(); + + for (const property of this.properties) + { // For each defined model property, assigning its deserialized value. + (this.instance[property.name as keyof T] as any) = property.definition.type.deserialize(serialized[property.name]); + } + + // Reset original property values. + this.resetDiff(); + // Store the original serialized object. + this.original.serialized = serialized; + + return this; + } + + /** + * Get current model instance identifier. + */ + getIdentifier(): IdentifierType + { + if (Array.isArray(this.definition.identifier)) + { // The identifier is composite, make an array of properties values. + return this.definition.identifier.map(identifier => this.instance?.[identifier as keyof T]) as IdentifierType; + } + else + { // The identifier is a simple property, get its value. + return this.instance?.[this.definition.identifier as keyof Shape as keyof T] as IdentifierType; + } + } + + /** + * Get current model instance properties. + */ + getInstanceProperties(): ModelPropertiesValues + { + // Initialize an empty model properties object. + const instanceProperties: Partial> = {}; + + for (const property of this.properties) + { // For each defined model property, adding it to the properties object. + instanceProperties[property.name] = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T. + } + + return instanceProperties as ModelPropertiesValues; // Returning the properties object. + } + + /** + * Serialize the model instance. + */ + serialize(): SerializedModel + { + // Creating an empty serialized object. + const serializedObject: Partial> = {}; + + for (const property of this.properties) + { // For each defined model property, adding it to the serialized object. + serializedObject[property.name] = property.definition.type.serialize( + // keyof Shape is a subset of keyof T. + this.instance?.[property.name as keyof T], + ); + } + + return serializedObject as SerializedModel; // Returning the serialized object. + } + + /** + * Examine if the model is new (never deserialized) or not. + */ + isNew(): boolean + { + return !this.original.serialized; + } + + /** + * Examine if the model is dirty or not. + */ + isDirty(): boolean + { + for (const property of this.properties) + { // For each property, check if it is different. + if (property.definition.type.hasChanged(this.original.properties?.[property.name], this.instance?.[property.name as keyof T])) + // There is a difference: the model is dirty. + return true; + } + + // No difference. + return false; + } + + /** + * Serialize the difference between current model state and the original one. + */ + serializeDiff(): Partial> + { + // Creating an empty serialized object. + const serializedObject: Partial> = {}; + + for (const property of this.properties) + { // For each defined model property, adding it to the serialized object if it has changed or if it is in the identifier. + const instancePropValue = this.instance?.[property.name as keyof T]; // keyof Shape is a subset of keyof T. + if ( + property.identifier || + property.definition.type.hasChanged(this.original.properties?.[property.name], instancePropValue) + ) // The property is part of the identifier or its value has changed. + serializedObject[property.name] = property.definition.type.serializeDiff(instancePropValue); + } + + return serializedObject; // Returning the serialized object. + } + + /** + * Set current model properties as original values. + */ + resetDiff(): void + { + this.original.properties = {}; + for (const property of this.properties) + { // For each property, set its original value to the current property value. + const instancePropValue = this.instance?.[property.name as keyof T]; + this.original.properties[property.name] = property.definition.type.clone(instancePropValue); + property.definition.type.resetDiff(instancePropValue); + } + } + + /** + * Get difference between original values and current ones, then reset it. + * Similar to call `serializeDiff()` then `resetDiff()`. + */ + patch(): Partial> + { + // Get the difference. + const diff = this.serializeDiff(); + + // Once the difference has been obtained, reset it. + this.resetDiff(); + + return diff; // Return the difference. + } + + /** + * Clone the model instance. + */ + clone(): ModelInstance + { + // Initialize a new instance for the clone. + const cloned = this.manager.model(); + + // Clone every value of the model instance. + for (const [key, value] of Object.entries(this.instance) as [keyof T, unknown][]) + { // For each [key, value], clone the value and put it in the cloned instance. + + // Do not clone ourselves. + if (key == "_sharkitek") continue; + + if (this.definition.properties[key]) + { // The current key is a defined property, clone using the defined type. + (cloned.instance[key] as any) = (this.definition.properties[key] as UnknownDefinition).type.clone(value); + } + else + { // Not a property, cloning the raw value. + (cloned.instance[key] as any) = structuredClone(value); + } + } + + // Clone original properties. + for (const property of this.properties) + { // For each property, clone its original value. + cloned.original.properties[property.name] = property.definition.type.clone(this.original.properties[property.name]); + } + + // Clone original serialized. + cloned.original.serialized = structuredClone(this.original.serialized); + + return cloned.instance; // Returning the cloned instance. + } +} + + + +/** + * A model manager, created from a model definition. + */ +export class ModelManager, Identifier extends IdentifierDefinition> +{ + /** + * Defined properties. + */ + properties: ModelProperties; + + /** + * Initialize a model manager from a model definition. + * @param definition The model definition. + */ + constructor(public readonly definition: ModelDefinition) + { + this.initProperties(); + } + + /** + * Initialize properties iterator from current definition. + * @protected + */ + protected initProperties(): void + { + // Build an array of model properties from the definition. + this.properties = []; + for (const propertyName in this.definition.properties) + { // For each property, build a model property object. + this.properties.push({ + name: propertyName, + definition: this.definition.properties[propertyName], + // Find out if the current property is part of the identifier. + identifier: ( + Array.isArray(this.definition.identifier) + // The identifier is an array, the property must be in the array. + ? this.definition.identifier.includes(propertyName as keyof Shape as keyof T) + // The identifier is a single string, the property must be the defined identifier. + : (this.definition.identifier == propertyName as keyof Shape) + ), + } as ModelProperty); + } + } + + /** + * Get the model state of the provided model instance. + * @param instance The model instance for which to get its state. NULL or undefined to create a new one. + */ + model(instance: T|ModelInstance|null = null): Model + { // Get the instance model state if there is one, or initialize a new one. + if (instance) + // There is an instance, create a model from it. + return ((instance as ModelInstance)?._sharkitek ?? (new Model(this))).fromInstance(instance); + else + // No instance, initialize a new one. + return (new Model(this)).initInstance(); + } + + /** + * Parse the serialized model object to a new model instance. + * @param serialized The serialized model object. + */ + parse(serialized: SerializedModel): ModelInstance + { + return this.model().deserialize(serialized).instance; + } +} + +/** + * Define a new model. + * @param definition The model definition object. + */ +export function defineModel, Identifier extends IdentifierDefinition>( + definition: ModelDefinition +) +{ + return new ModelManager(definition); +} + +/** + * A generic model manager for a provided model type, to use in circular dependencies. + */ +export type GenericModelManager = ModelManager, IdentifierDefinition>>; + +/** + * Function to get a model manager lazily. + */ +export type LazyModelManager, Identifier extends IdentifierDefinition> + = (() => ModelManager); +/** + * A model manager definition that can be lazy. + */ +export type DeclaredModelManager, Identifier extends IdentifierDefinition> + = ModelManager|LazyModelManager; diff --git a/src/model/properties.ts b/src/model/properties.ts new file mode 100644 index 0000000..fe32339 --- /dev/null +++ b/src/model/properties.ts @@ -0,0 +1,10 @@ +export {define} from "./property-definition"; + +export {array} from "./types/array"; +export {bool, boolean} from "./types/boolean"; +export {date} from "./types/date"; +export {decimal} from "./types/decimal"; +export {model} from "./types/model"; +export {numeric} from "./types/numeric"; +export {object} from "./types/object"; +export {string} from "./types/string"; diff --git a/src/Model/PropertyDefinition.ts b/src/model/property-definition.ts similarity index 72% rename from src/Model/PropertyDefinition.ts rename to src/model/property-definition.ts index 60b8fe8..c5f8d86 100644 --- a/src/Model/PropertyDefinition.ts +++ b/src/model/property-definition.ts @@ -1,4 +1,4 @@ -import {Type} from "./Types/Type"; +import {Type} from "./types/type"; /** * Property definition class. @@ -16,6 +16,16 @@ export class Definition {} } +/** + * Unknown property definition. + */ +export type UnknownDefinition = Definition; + +/** + * Any property definition. + */ +export type AnyDefinition = Definition; + /** * New definition of a property of the given type. * @param type Type of the property to define. diff --git a/src/Model/Types/ArrayType.ts b/src/model/types/array.ts similarity index 71% rename from src/Model/Types/ArrayType.ts rename to src/model/types/array.ts index ee0bd71..1fbeab8 100644 --- a/src/Model/Types/ArrayType.ts +++ b/src/model/types/array.ts @@ -1,5 +1,5 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; /** * Type of an array of values. @@ -55,16 +55,16 @@ export class ArrayType extends Type this.valueDefinition.type.resetDiff(value)); } - propertyHasChanged(originalValue: SharkitekValueType[]|null|undefined, currentValue: SharkitekValueType[]|null|undefined): boolean + hasChanged(originalValue: SharkitekValueType[]|null|undefined, currentValue: SharkitekValueType[]|null|undefined): boolean { // If any array length is different, arrays are different. if (originalValue?.length != currentValue?.length) return true; // If length is undefined, values are probably not arrays. - if (originalValue?.length == undefined) return false; + if (originalValue?.length == undefined) return super.hasChanged(originalValue, currentValue); for (const key of originalValue.keys()) { // Check for any change for each value in the array. - if (this.valueDefinition.type.propertyHasChanged(originalValue[key], currentValue[key])) + if (this.valueDefinition.type.hasChanged(originalValue[key], currentValue[key])) // The value has changed, the array is different. return true; } @@ -72,22 +72,38 @@ export class ArrayType extends Type(array: T|null|undefined): T + { + // Handle NULL / undefined array. + if (!array) return super.clone(array); + + // Initialize an empty array. + const cloned = [] as T; + + for (const value of array) + { // Clone each value of the array. + cloned.push(this.valueDefinition.type.clone(value)); + } + + return cloned; // Returning cloned array. + } } /** diff --git a/src/Model/Types/BoolType.ts b/src/model/types/boolean.ts similarity index 64% rename from src/Model/Types/BoolType.ts rename to src/model/types/boolean.ts index 8bba75a..77e26c8 100644 --- a/src/Model/Types/BoolType.ts +++ b/src/model/types/boolean.ts @@ -1,10 +1,10 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; /** * Type of any boolean value. */ -export class BoolType extends Type +export class BooleanType extends Type { deserialize(value: boolean|null|undefined): boolean|null|undefined { @@ -28,15 +28,15 @@ export class BoolType extends Type /** * New boolean property definition. */ -export function bool(): Definition +export function boolean(): Definition { - return define(new BoolType()); + return define(new BooleanType()); } /** * New boolean property definition. - * Alias of bool. + * Alias of boolean. */ -export function boolean(): ReturnType +export function bool(): ReturnType { - return bool(); + return boolean(); } diff --git a/src/model/types/date.ts b/src/model/types/date.ts new file mode 100644 index 0000000..83261ef --- /dev/null +++ b/src/model/types/date.ts @@ -0,0 +1,51 @@ +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; + +/** + * Type of dates. + */ +export class DateType extends Type +{ + 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|null|undefined): string|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + if (isNaN(value?.valueOf())) return value?.toString(); + + return value?.toISOString(); + } + + hasChanged(originalValue: Date|null|undefined, currentValue: Date|null|undefined): boolean + { + if (originalValue instanceof Date && currentValue instanceof Date) + { // Compare dates. + const originalTime = originalValue.getTime(); + const currentTime = currentValue.getTime(); + + // The two values are not numbers, nothing has changed. + if (isNaN(originalTime) && isNaN(currentTime)) return false; + + // Timestamps need to be exactly the same. + return originalValue.getTime() !== currentValue.getTime(); + } + else + // Compare undefined or null values. + return originalValue !== currentValue; + } +} + +/** + * New date property definition. + */ +export function date(): Definition +{ + return define(new DateType()); +} diff --git a/src/Model/Types/DecimalType.ts b/src/model/types/decimal.ts similarity index 87% rename from src/Model/Types/DecimalType.ts rename to src/model/types/decimal.ts index 4bb1438..d8bba75 100644 --- a/src/Model/Types/DecimalType.ts +++ b/src/model/types/decimal.ts @@ -1,5 +1,5 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; /** * Type of decimal numbers. diff --git a/src/model/types/model.ts b/src/model/types/model.ts new file mode 100644 index 0000000..1a1de84 --- /dev/null +++ b/src/model/types/model.ts @@ -0,0 +1,116 @@ +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; +import { + GenericModelManager, + IdentifierDefinition, DeclaredModelManager, + ModelInstance, + ModelManager, + ModelShape, + SerializedModel +} from "../model"; + +/** + * Type of a Sharkitek model value. + */ +export class ModelType, Identifier extends IdentifierDefinition> extends Type, ModelInstance> +{ + /** + * Initialize a new model type of a Sharkitek model property. + * @param declaredModelManager Model manager. + */ + constructor(protected declaredModelManager: DeclaredModelManager) + { + super(); + } + + /** + * Resolve the defined model using the declared model, that can be defined lazily. + */ + get definedModel(): ModelManager + { + return typeof this.declaredModelManager == "object" ? this.declaredModelManager : this.declaredModelManager(); + } + + serialize(value: ModelInstance|null|undefined): SerializedModel|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Serializing the given model. + return this.definedModel.model(value).serialize(); + } + + deserialize(value: SerializedModel|null|undefined): ModelInstance|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Parse the given object in the new model. + return this.definedModel.parse(value); + } + + serializeDiff(value: ModelInstance|null|undefined): Partial>|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Serializing the given model. + return this.definedModel.model(value).serializeDiff(); + } + + resetDiff(value: ModelInstance|null|undefined): void + { + // Reset diff of the given model. + this.definedModel.model(value).resetDiff(); + } + + hasChanged(originalValue: ModelInstance|null|undefined, currentValue: ModelInstance|null|undefined): boolean + { + if (originalValue === undefined) return currentValue !== undefined; + if (originalValue === null) return currentValue !== null; + if (currentValue === undefined) return true; // Original value is not undefined. + if (currentValue === null) return true; // Original value is not null. + + // If the current value is dirty, it has changed. + return this.definedModel.model(currentValue).isDirty(); + } + + serializedHasChanged(originalValue: SerializedModel | null | undefined, currentValue: SerializedModel | null | undefined): boolean + { + if (originalValue === undefined) return currentValue !== undefined; + if (originalValue === null) return currentValue !== null; + if (currentValue === undefined) return true; // Original value is not undefined. + if (currentValue === null) return true; // Original value is not null. + + // If any property has changed, the value has changed. + for (const property of this.definedModel.properties) + if (property.definition.type.serializedHasChanged(originalValue?.[property.name], currentValue?.[property.name])) + return true; + + return false; // No change detected. + } + + clone>(value: Type|null|undefined): Type + { + // Handle NULL / undefined values. + if (!value) return super.clone(value); + + return this.definedModel.model(value).clone() as Type; + } +} + +/** + * New model property definition. + * @param definedModel Model manager. + */ +export function model, Identifier extends IdentifierDefinition>( + definedModel: DeclaredModelManager +): Definition, ModelInstance> +{ + return define(new ModelType(definedModel)); +} + +export function circular(definedModel: () => any): () => GenericModelManager +{ + return definedModel; +} diff --git a/src/Model/Types/NumericType.ts b/src/model/types/numeric.ts similarity index 82% rename from src/Model/Types/NumericType.ts rename to src/model/types/numeric.ts index 098965d..2236e17 100644 --- a/src/Model/Types/NumericType.ts +++ b/src/model/types/numeric.ts @@ -1,5 +1,5 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; /** * Type of any numeric value. diff --git a/src/model/types/object.ts b/src/model/types/object.ts new file mode 100644 index 0000000..b308c01 --- /dev/null +++ b/src/model/types/object.ts @@ -0,0 +1,159 @@ +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; +import {ModelProperties, ModelPropertiesValues, ModelProperty, ModelShape, SerializedModel} from "../model"; + +/** + * Type of a custom object. + */ +export class ObjectType, T extends object> extends Type, ModelPropertiesValues> +{ + /** + * Defined properties. + */ + properties: ModelProperties; + + /** + * Initialize a new object type of a Sharkitek model property. + * @param shape + */ + constructor(readonly shape: Shape) + { + super(); + this.initProperties(); + } + + /** + * Initialize properties iterator from the object shape. + * @protected + */ + protected initProperties(): void + { + // Build an array of model properties from the object shape. + this.properties = []; + for (const propertyName in this.shape) + { // For each property, build a model property object. + this.properties.push({ + name: propertyName, + definition: this.shape[propertyName], + identifier: false, + } as ModelProperty); + } + } + + deserialize(value: SerializedModel|null|undefined): ModelPropertiesValues|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Initialize an empty object. + const obj: Partial> = {}; + + for (const property of this.properties) + { // For each defined property, deserialize its value according to its type. + (obj[property.name as keyof T] as any) = property.definition.type.deserialize(value?.[property.name]); + } + + return obj as ModelPropertiesValues; // Returning serialized object. + } + + serialize(value: ModelPropertiesValues|null|undefined): SerializedModel|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Creating an empty serialized object. + const serializedObject: Partial> = {}; + + for (const property of this.properties) + { // For each property, adding it to the serialized object. + serializedObject[property.name] = property.definition.type.serialize( + // keyof Shape is a subset of keyof T. + value?.[property.name as keyof T], + ); + } + + return serializedObject as SerializedModel; // Returning the serialized object. + } + + serializeDiff(value: ModelPropertiesValues|null|undefined): Partial>|null|undefined + { + if (value === undefined) return undefined; + if (value === null) return null; + + // Creating an empty serialized object. + const serializedObject: Partial> = {}; + + for (const property of this.properties) + { // For each property, adding it to the serialized object. + serializedObject[property.name] = property.definition.type.serializeDiff( + // keyof Shape is a subset of keyof T. + value?.[property.name as keyof T], + ); + } + + return serializedObject as SerializedModel; // Returning the serialized object. + } + + resetDiff(value: ModelPropertiesValues|null|undefined) + { + // For each property, reset its diff. + for (const property of this.properties) + // keyof Shape is a subset of keyof T. + property.definition.type.resetDiff(value?.[property.name as keyof T]); + } + + hasChanged(originalValue: ModelPropertiesValues|null|undefined, currentValue: ModelPropertiesValues|null|undefined): boolean + { + if (originalValue === undefined) return currentValue !== undefined; + if (originalValue === null) return currentValue !== null; + if (currentValue === undefined) return true; // Original value is not undefined. + if (currentValue === null) return true; // Original value is not null. + + // If any property has changed, the value has changed. + for (const property of this.properties) + if (property.definition.type.hasChanged(originalValue?.[property.name as keyof T], currentValue?.[property.name as keyof T])) + return true; + + return false; // No change detected. + } + + serializedHasChanged(originalValue: SerializedModel|null|undefined, currentValue: SerializedModel|null|undefined): boolean + { + if (originalValue === undefined) return currentValue !== undefined; + if (originalValue === null) return currentValue !== null; + if (currentValue === undefined) return true; // Original value is not undefined. + if (currentValue === null) return true; // Original value is not null. + + // If any property has changed, the value has changed. + for (const property of this.properties) + if (property.definition.type.serializedHasChanged(originalValue?.[property.name], currentValue?.[property.name])) + return true; + + return false; // No change detected. + } + + clone>(value: Type|null|undefined): Type + { + // Handle NULL / undefined object. + if (!value) return super.clone(value); + + // Initialize an empty object. + const cloned: Partial> = {}; + + for (const property of this.properties) + { // For each defined property, clone it. + cloned[property.name as keyof T] = property.definition.type.clone(value?.[property.name]); + } + + return cloned as Type; // Returning cloned object. + } +} + +/** + * New object property definition. + * @param shape Shape of the object. + */ +export function object, T extends object>(shape: Shape): Definition, ModelPropertiesValues> +{ + return define(new ObjectType(shape)); +} diff --git a/src/Model/Types/StringType.ts b/src/model/types/string.ts similarity index 82% rename from src/Model/Types/StringType.ts rename to src/model/types/string.ts index b1b7267..b432b4a 100644 --- a/src/Model/Types/StringType.ts +++ b/src/model/types/string.ts @@ -1,5 +1,5 @@ -import {Type} from "./Type"; -import {define, Definition} from "../PropertyDefinition"; +import {Type} from "./type"; +import {define, Definition} from "../property-definition"; /** * Type of any string value. diff --git a/src/Model/Types/Type.ts b/src/model/types/type.ts similarity index 50% rename from src/Model/Types/Type.ts rename to src/model/types/type.ts index d1682e4..6c636df 100644 --- a/src/Model/Types/Type.ts +++ b/src/model/types/type.ts @@ -11,13 +11,13 @@ export abstract class Type /** * Deserialize the given value of a serialized Sharkitek model. - * @param value - Value to deserialize. + * @param value Value to deserialize. */ abstract deserialize(value: SerializedType|null|undefined): ModelType|null|undefined; /** * Serialize the given value only if it has changed. - * @param value - Value to deserialize. + * @param value Value to deserialize. */ serializeDiff(value: ModelType|null|undefined): Partial|null|undefined { @@ -26,7 +26,7 @@ export abstract class Type /** * Reset the difference between the original value and the current one. - * @param value - Value for which reset diff data. + * @param value Value for which reset diff data. */ resetDiff(value: ModelType|null|undefined): void { @@ -34,22 +34,31 @@ export abstract class Type } /** - * Determine if the property value has changed. - * @param originalValue - Original property value. - * @param currentValue - Current property value. + * Determine if the value has changed. + * @param originalValue Original value. + * @param currentValue Current value. */ - propertyHasChanged(originalValue: ModelType|null|undefined, currentValue: ModelType|null|undefined): boolean + hasChanged(originalValue: ModelType|null|undefined, currentValue: ModelType|null|undefined): boolean { - return originalValue != currentValue; + return originalValue !== currentValue; } /** - * Determine if the serialized property value has changed. - * @param originalValue - Original serialized property value. - * @param currentValue - Current serialized property value. + * Determine if the serialized value has changed. + * @param originalValue Original serialized value. + * @param currentValue Current serialized value. */ - serializedPropertyHasChanged(originalValue: SerializedType|null|undefined, currentValue: SerializedType|null|undefined): boolean + serializedHasChanged(originalValue: SerializedType|null|undefined, currentValue: SerializedType|null|undefined): boolean { - return originalValue != currentValue; + return originalValue !== currentValue; + } + + /** + * Clone the provided value. + * @param value The to clone. + */ + clone(value: T|null|undefined): T + { + return structuredClone(value); } } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5f748b7 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,4 @@ +/** + * Type definition of a class constructor. + */ +export type ConstructorOf = { new(): T; }; diff --git a/tests/Model.test.ts b/tests/Model.test.ts deleted file mode 100644 index fa83ab4..0000000 --- a/tests/Model.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import {s} from "../src"; - -/** - * Another test 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(), -}).extends({ - extension(): string - { - return this.name; - } -}) -{ - active: boolean = true; - - constructor(name: string = "", firstName: string = "", email: string = "", createdAt: Date = new Date()) - { - super(); - - this.name = name; - this.firstName = firstName; - this.email = email; - this.createdAt = createdAt; - } -} - -/** - * A test 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; - authors: Author[] = []; - text: string; - evaluation: number; - tags: { - name: string; - }[]; -} - -it("deserialize", () => { - expect((new Article()).deserialize({ - id: 1, - title: "this is a test", - authors: [ - { name: "DOE", firstName: "John", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, - { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, - ], - text: "this is a long test.", - evaluation: "25.23", - tags: [ {name: "test"}, {name: "foo"} ], - }).serialize()).toStrictEqual({ - id: 1, - title: "this is a test", - authors: [ - { name: "DOE", firstName: "John", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, - { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, - ], - text: "this is a long test.", - evaluation: "25.23", - tags: [ {name: "test"}, {name: "foo"} ], - }); -}); - -it("create and check state then serialize", () => { - const now = new Date(); - const article = new Article(); - article.id = 1; - article.title = "this is a test"; - article.authors = [ - new Author("DOE", "John", "test@test.test", now), - ]; - article.text = "this is a long test."; - article.evaluation = 25.23; - article.tags = []; - article.tags.push({name: "test"}); - article.tags.push({name: "foo"}); - - expect(article.isNew()).toBeTruthy(); - expect(article.getIdentifier()).toStrictEqual(1); - - expect(article.serialize()).toStrictEqual({ - id: 1, - title: "this is a test", - authors: [ - { name: "DOE", firstName: "John", email: "test@test.test", createdAt: now.toISOString(), active: true, }, - ], - text: "this is a long test.", - evaluation: "25.23", - tags: [ {name: "test"}, {name: "foo"} ], - }); -}); - - -it("deserialize then patch", () => { - const article = (new Article()).deserialize({ - id: 1, - title: "this is a test", - authors: [ - { 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", - tags: [ {name: "test"}, {name: "foo"} ], - }); - - expect(article.isNew()).toBeFalsy(); - expect(article.isDirty()).toBeFalsy(); - expect(article.evaluation).toStrictEqual(25.23); - - article.text = "Modified text."; - - expect(article.isDirty()).toBeTruthy(); - - expect(article.patch()).toStrictEqual({ - id: 1, - text: "Modified text.", - }); -}); - -it("patch with modified submodels", () => { - const article = (new Article()).deserialize({ - id: 1, - title: "this is a test", - authors: [ - { 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("1997-09-09")).toISOString(), active: false, }, - ], - text: "this is a long test.", - evaluation: "25.23", - tags: [ {name: "test"}, {name: "foo"} ], - }); - - article.authors[0].name = "TEST"; - article.authors[1].createdAt.setMonth(9); - - expect(article.patch()).toStrictEqual({ - id: 1, - authors: [ - { name: "TEST" }, - { createdAt: (new Date("1997-10-09")).toISOString() }, //{ name: "TEST", firstName: "Another", email: "another@test.test" }, - ], - }); -}); - -it("test author extension", () => { - const author = new Author(); - author.name = "test name"; - expect(author.extension()).toStrictEqual("test name"); -}); diff --git a/tests/model.test.ts b/tests/model.test.ts new file mode 100644 index 0000000..d6c8393 --- /dev/null +++ b/tests/model.test.ts @@ -0,0 +1,329 @@ +import {describe, expect, it} from "vitest"; +import {circular, defineModel, s} from "../src/library"; + +/** + * Test class of an account. + */ +class Account +{ + static model = s.defineModel({ + Class: Account, + identifier: "id", + properties: { + id: s.property.numeric(), + createdAt: s.property.date(), + name: s.property.string(), + email: s.property.string(), + active: s.property.boolean(), + }, + }); + + id: number; + createdAt: Date; + name: string; + email: string; + active: boolean; +} + +/** + * Test class of an article. + */ +class Article +{ + static model = s.defineModel({ + Class: Article, + identifier: "id", + properties: { + id: s.property.numeric(), + title: s.property.string(), + authors: s.property.array(s.property.model(() => Account.model)), + text: s.property.string(), + evaluation: s.property.decimal(), + tags: s.property.array(s.property.object({ name: s.property.string() })), + comments: s.property.array(s.property.model(() => ArticleComment.model)), + }, + }); + + id: number; + title: string; + authors: Account[]; + text: string; + evaluation: number; + tags: { name: string }[]; + comments: ArticleComment[]; +} + +/** + * Test class of a comment on an article. + */ +class ArticleComment +{ + static model = s.defineModel({ + Class: ArticleComment, + identifier: "id", + properties: { + id: s.property.numeric(), + article: s.property.model(circular
(() => Article.model)), + author: s.property.model(() => Account.model), + message: s.property.string(), + }, + }); + + id: number; + article?: Article; + author: Account; + message: string; +} + +/** + * Get a test account instance. + */ +function getTestAccount(): Account +{ + const account = new Account(); + account.id = 52; + account.createdAt = new Date(); + account.name = "John Doe"; + account.email = "john@doe.test"; + account.active = true; + return account; +} + +function getTestArticle(): Article +{ + const article = new Article(); + article.id = 1; + article.title = "this is a test"; + article.text = "this is a long test."; + article.evaluation = 25.23; + article.tags = [ + { name: "test" }, + { name: "foo" }, + ]; + article.authors = [getTestAccount()]; + article.comments = []; + return article; +} + +describe("model", () => { + it("initializes a new model", () => { + const article = getTestArticle(); + const newModel = Article.model.model(article); + expect(newModel.instance).toBe(article); + }); + it("gets a model state from its instance", () => { + const article = getTestArticle(); + expect(Article.model.model(article).isNew()).toBeTruthy(); + expect(Article.model.model(article).isDirty()).toBeFalsy(); + }); + it("gets a model identifier value", () => { + const article = getTestArticle(); + expect(Article.model.model(article).getIdentifier()).toBe(1); + }); + it("gets a model composite identifier value", () => { + class CompositeModel + { + static model = s.defineModel({ + Class: CompositeModel, + properties: { + firstId: s.property.numeric(), + secondId: s.property.numeric(), + label: s.property.string(), + }, + identifier: ["firstId", "secondId"], + }) + + firstId: number; + secondId: number; + label: string; + } + + expect( + CompositeModel.model.model(Object.assign(new CompositeModel(), { + firstId: 5, + secondId: 6, + label: "test", + })).getIdentifier() + ).toStrictEqual([5, 6]); + }); + it("checks model dirtiness when altered, then reset diff", () => { + const article = getTestArticle(); + expect(Article.model.model(article).isDirty()).toBeFalsy(); + article.title = "new title"; + expect(Article.model.model(article).isDirty()).toBeTruthy(); + Article.model.model(article).resetDiff() + expect(Article.model.model(article).isDirty()).toBeFalsy(); + }); + + it("deserializes a model from a serialized form", () => { + const expectedArticle = Object.assign(new Article(), { + id: 1, + title: "this is a test", + authors: [ + Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }), + Object.assign(new Account(), { id: 4, name: "Tester", email: "another@test.test", createdAt: new Date("2022-09-07T18:32:55.000Z"), active: false, }), + ], + text: "this is a long test.", + evaluation: 8.52, + tags: [ {name: "test"}, {name: "foo"} ], + comments: [ + Object.assign(new ArticleComment(), { id: 542, author: Object.assign(new Account(), { id: 52, name: "John Doe", email: "test@test.test", createdAt: new Date("2022-08-07T08:47:01.000Z"), active: true, }), message: "comment content", }), + ], + }); + + const deserializedArticle = Article.model.parse({ + id: 1, + title: "this is a test", + authors: [ + { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, + { id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, + ], + text: "this is a long test.", + evaluation: "8.52", + tags: [ {name: "test"}, {name: "foo"} ], + comments: [ + { id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, + ], + }); + + const deserializedArticleProperties = Article.model.model(deserializedArticle).getInstanceProperties(); + delete deserializedArticleProperties.authors[0]._sharkitek; + delete deserializedArticleProperties.authors[1]._sharkitek; + delete deserializedArticleProperties.comments[0]._sharkitek; + delete (deserializedArticleProperties.comments[0].author as any)._sharkitek; + const expectedArticleProperties = Article.model.model(expectedArticle).getInstanceProperties(); + delete expectedArticleProperties.authors[0]._sharkitek; + delete expectedArticleProperties.authors[1]._sharkitek; + delete expectedArticleProperties.comments[0]._sharkitek; + delete (expectedArticleProperties.comments[0].author as any)._sharkitek; + expect(deserializedArticleProperties).toEqual(expectedArticleProperties); + }); + + it("serializes an initialized model", () => { + const article = getTestArticle(); + expect(Article.model.model(article).serialize()).toEqual({ + id: 1, + title: "this is a test", + text: "this is a long test.", + evaluation: "25.23", + tags: [{ name: "test" }, { name: "foo" }], + authors: [ + { id: 52, createdAt: article.authors[0].createdAt.toISOString(), name: "John Doe", email: "john@doe.test", active: true } + ], + comments: [], + }); + }); + + it("deserializes, changes and patches", () => { + const deserializedArticle = Article.model.parse({ + id: 1, + title: "this is a test", + authors: [ + { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, + { id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, + ], + text: "this is a long test.", + evaluation: "8.52", + tags: [ {name: "test"}, {name: "foo"} ], + comments: [ + { id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, + ], + }); + + deserializedArticle.text = "A new text for a new life!"; + + expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({ + id: 1, + text: "A new text for a new life!", + }); + + deserializedArticle.evaluation = 5.24; + + expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({ + id: 1, + evaluation: "5.24", + }); + }); + + it("patches with modified submodels", () => { + const deserializedArticle = Article.model.parse({ + id: 1, + title: "this is a test", + authors: [ + { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, + { id: 4, name: "Tester", email: "another@test.test", createdAt: "2022-09-07T18:32:55.000Z", active: false, }, + ], + text: "this is a long test.", + evaluation: "8.52", + tags: [ {name: "test"}, {name: "foo"} ], + comments: [ + { id: 542, author: { id: 52, name: "John Doe", email: "test@test.test", createdAt: "2022-08-07T08:47:01.000Z", active: true, }, message: "comment content", }, + ], + }); + + deserializedArticle.authors[1].active = true; + + expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({ + id: 1, + authors: [ + { id: 52, }, + { id: 4, active: true }, + ], + }); + + deserializedArticle.comments[0].author.name = "Johnny"; + + expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({ + id: 1, + comments: [ + { + id: 542, + author: { + id: 52, + name: "Johnny", + }, + }, + ], + }); + }); + + it("deserializes and patches with fields that are not properties", () => { + class TestModel + { + static model = defineModel({ + Class: TestModel, + properties: { + id: s.property.numeric(), + label: s.property.string(), + }, + identifier: "id", + }) + + id: number; + label: string; + + notAProperty: { hello: string } = { hello: "world" }; + } + + const deserializedModel = TestModel.model.parse({ + id: 5, + label: "testing", + }); + expect(deserializedModel.id).toBe(5); + expect(deserializedModel.label).toBe("testing"); + expect(deserializedModel.notAProperty?.hello).toBe("world"); + + const clonedDeserializedModel = TestModel.model.model(deserializedModel).clone(); + + deserializedModel.label = "new!"; + expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5, label: "new!" }); + + deserializedModel.notAProperty.hello = "monster"; + expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({ id: 5 }); + + expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({ id: 5, label: "new!" }); + + expect(TestModel.model.model(clonedDeserializedModel).serialize()).toStrictEqual({ id: 5, label: "testing" }); + expect(clonedDeserializedModel.notAProperty.hello).toEqual("world"); + }); +}); diff --git a/tests/model/types/array.test.ts b/tests/model/types/array.test.ts new file mode 100644 index 0000000..2a775cc --- /dev/null +++ b/tests/model/types/array.test.ts @@ -0,0 +1,125 @@ +import {describe, expect, test} from "vitest"; +import {ArrayType, s} from "../../../src/library"; + +class TestModel +{ + id: number; + name: string; + price: number; +} + +describe("array type", () => { + const testModel = s.defineModel({ + Class: TestModel, + properties: { + id: s.property.numeric(), + name: s.property.string(), + price: s.property.decimal(), + }, + identifier: "id", + }); + + test("array type definition", () => { + const arrayType = s.property.array(s.property.model(testModel)); + expect(arrayType.type).toBeInstanceOf(ArrayType); + }); + + const testProperty = s.property.array(s.property.decimal()); + + test("array type functions", () => { + expect(testProperty.type.serialize([12.547, 8, -52.11])).toEqual(["12.547", "8", "-52.11"]); + expect(testProperty.type.deserialize(["12.547", "8", "-52.11"])).toEqual([12.547, 8, -52.11]); + + { // Try to serialize the difference of an array with one changed model. + const propertyValue = [ + testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance, + testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance, + ]; + propertyValue[0].name = "new"; + expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([ + { id: 1, name: "new" }, + { id: 2 }, + ]); + } + + expect(testProperty.type.serialize(null)).toBe(null); + expect(testProperty.type.deserialize(null)).toBe(null); + expect(testProperty.type.serializeDiff(null)).toBe(null); + + expect(testProperty.type.serialize(undefined)).toBe(undefined); + expect(testProperty.type.deserialize(undefined)).toBe(undefined); + expect(testProperty.type.serializeDiff(undefined)).toBe(undefined); + + expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8, -52.11])).toBeFalsy(); + expect(testProperty.type.hasChanged(null, null)).toBeFalsy(); + expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy(); + expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy(); + expect(testProperty.type.hasChanged(null, [12.547, 8, -52.11])).toBeTruthy(); + expect(testProperty.type.hasChanged(undefined, [12.547, 8, -52.11])).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, 8, -52.11], null)).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, 8, -52.11], undefined)).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, -52.11, 8])).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, -52.11, 8], [12.547, 8, -52.11])).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, 8, -52.11], [12.547, 8])).toBeTruthy(); + expect(testProperty.type.hasChanged([12.547, 8], [12.547, 8, -52.11])).toBeTruthy(); + + expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "8", "-52.11"])).toBeFalsy(); + expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy(); + expect(testProperty.type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(testProperty.type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(null, ["12.547", "8", "-52.11"])).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(undefined, ["12.547", "8", "-52.11"])).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], null)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], undefined)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "-52.11", "8"])).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "-52.11", "8"], ["12.547", "8", "-52.11"])).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "8", "-52.11"], ["12.547", "8"])).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(["12.547", "8"], ["12.547", "8", "-52.11"])).toBeTruthy(); + + { // Try to reset the difference of an array with one changed model. + const propertyValue = [ + testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance, + testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance, + ]; + propertyValue[0].name = "new"; + expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([ + { id: 1, name: "new" }, + { id: 2 }, + ]); + s.property.array(s.property.model(testModel)).type.resetDiff(propertyValue) + expect(s.property.array(s.property.model(testModel)).type.serializeDiff(propertyValue)).toEqual([ + { id: 1 }, + { id: 2 }, + ]); + } + testProperty.type.resetDiff(undefined); + testProperty.type.resetDiff(null); + + { // Test that values are cloned in a different array. + const propertyValue = [12.547, 8, -52.11]; + const clonedPropertyValue = testProperty.type.clone(propertyValue); + expect(clonedPropertyValue).not.toBe(propertyValue); + expect(clonedPropertyValue).toEqual(propertyValue); + } + { // Test that values are cloned recursively. + const propertyValue = [ + testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 22 })).instance, + testModel.model(Object.assign(new TestModel(), { id: 2, name: "another", price: 12.55 })).instance, + ]; + + // The arrays are different. + const clonedPropertyValue = s.property.array(s.property.model(testModel)).type.clone(propertyValue); + expect(clonedPropertyValue).not.toBe(propertyValue); + + // Array values must be different objects but have the same values. + expect(clonedPropertyValue[0]).not.toBe(propertyValue[0]); + expect(clonedPropertyValue[1]).not.toBe(propertyValue[1]); + expect(testModel.model(clonedPropertyValue[0]).getInstanceProperties()).toEqual(testModel.model(propertyValue[0]).getInstanceProperties()); + expect(testModel.model(clonedPropertyValue[1]).getInstanceProperties()).toEqual(testModel.model(propertyValue[1]).getInstanceProperties()); + } + expect(testProperty.type.clone(undefined)).toBe(undefined); + expect(testProperty.type.clone(null)).toBe(null); + }); +}); diff --git a/tests/model/types/boolean.test.ts b/tests/model/types/boolean.test.ts new file mode 100644 index 0000000..44dbabf --- /dev/null +++ b/tests/model/types/boolean.test.ts @@ -0,0 +1,53 @@ +import {describe, expect, test} from "vitest"; +import {BooleanType, s, StringType} from "../../../src/library"; + +describe("boolean type", () => { + test("boolean type definition", () => { + { + const booleanType = s.property.boolean(); + expect(booleanType.type).toBeInstanceOf(BooleanType); + } + { + const boolType = s.property.bool(); + expect(boolType.type).toBeInstanceOf(BooleanType); + } + }); + + test("boolean type functions", () => { + expect(s.property.boolean().type.serialize(false)).toBe(false); + expect(s.property.boolean().type.deserialize(false)).toBe(false); + expect(s.property.boolean().type.serializeDiff(true)).toBe(true); + + expect(s.property.boolean().type.serialize(null)).toBe(null); + expect(s.property.boolean().type.deserialize(null)).toBe(null); + expect(s.property.boolean().type.serializeDiff(null)).toBe(null); + + expect(s.property.boolean().type.serialize(undefined)).toBe(undefined); + expect(s.property.boolean().type.deserialize(undefined)).toBe(undefined); + expect(s.property.boolean().type.serializeDiff(undefined)).toBe(undefined); + + expect(s.property.boolean().type.hasChanged(true, true)).toBeFalsy(); + expect(s.property.boolean().type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.boolean().type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.boolean().type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.boolean().type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.boolean().type.hasChanged(null, false)).toBeTruthy(); + expect(s.property.boolean().type.hasChanged(undefined, false)).toBeTruthy(); + expect(s.property.boolean().type.hasChanged(false, null)).toBeTruthy(); + expect(s.property.boolean().type.hasChanged(false, undefined)).toBeTruthy(); + + expect(s.property.boolean().type.serializedHasChanged(false, false)).toBeFalsy(); + expect(s.property.boolean().type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.boolean().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.boolean().type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.boolean().type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.boolean().type.serializedHasChanged(null, false)).toBeTruthy(); + expect(s.property.boolean().type.serializedHasChanged(undefined, false)).toBeTruthy(); + expect(s.property.boolean().type.serializedHasChanged(false, null)).toBeTruthy(); + expect(s.property.boolean().type.serializedHasChanged(false, undefined)).toBeTruthy(); + + s.property.boolean().type.resetDiff(false); + s.property.boolean().type.resetDiff(undefined); + s.property.boolean().type.resetDiff(null); + }); +}); diff --git a/tests/model/types/date.test.ts b/tests/model/types/date.test.ts new file mode 100644 index 0000000..1b4ebca --- /dev/null +++ b/tests/model/types/date.test.ts @@ -0,0 +1,62 @@ +import {describe, expect, test} from "vitest"; +import {DateType, s} from "../../../src/library"; + +describe("date type", () => { + const testDate = new Date(); + + test("date type definition", () => { + const dateType = s.property.date(); + expect(dateType.type).toBeInstanceOf(DateType); + }); + + test("date type functions", () => { + expect(s.property.date().type.serialize(testDate)).toBe(testDate.toISOString()); + expect(s.property.date().type.deserialize(testDate.toISOString())?.getTime()).toBe(testDate.getTime()); + expect(s.property.date().type.serializeDiff(new Date(testDate))).toBe(testDate.toISOString()); + expect(s.property.date().type.deserialize("2565152-2156121-256123121 5121544175:21515612").valueOf()).toBeNaN(); + expect(s.property.date().type.serialize(new Date(NaN))).toBe((new Date(NaN)).toString()); + + expect(s.property.date().type.serialize(null)).toBe(null); + expect(s.property.date().type.deserialize(null)).toBe(null); + expect(s.property.date().type.serializeDiff(null)).toBe(null); + + expect(s.property.date().type.serialize(undefined)).toBe(undefined); + expect(s.property.date().type.deserialize(undefined)).toBe(undefined); + expect(s.property.date().type.serializeDiff(undefined)).toBe(undefined); + + expect(s.property.date().type.hasChanged(testDate, new Date(testDate))).toBeFalsy(); + expect(s.property.date().type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.date().type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.date().type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.date().type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.date().type.hasChanged(null, testDate)).toBeTruthy(); + expect(s.property.date().type.hasChanged(undefined, testDate)).toBeTruthy(); + expect(s.property.date().type.hasChanged(testDate, null)).toBeTruthy(); + expect(s.property.date().type.hasChanged(new Date(NaN), null)).toBeTruthy(); + expect(s.property.date().type.hasChanged(new Date(NaN), undefined)).toBeTruthy(); + expect(s.property.date().type.hasChanged(new Date(NaN), new Date(NaN))).toBeFalsy(); + + expect(s.property.date().type.serializedHasChanged(testDate.toISOString(), (new Date(testDate)).toISOString())).toBeFalsy(); + expect(s.property.date().type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.date().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.date().type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged(null, testDate.toISOString())).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged(undefined, testDate.toISOString())).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged(testDate.toISOString(), null)).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), null)).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), undefined)).toBeTruthy(); + expect(s.property.date().type.serializedHasChanged((new Date(NaN)).toString(), (new Date(NaN)).toString())).toBeFalsy(); + + s.property.date().type.resetDiff(testDate); + s.property.date().type.resetDiff(undefined); + s.property.date().type.resetDiff(null); + + { // Test that the date is cloned in a different object. + const propertyValue = new Date(); + const clonedPropertyValue = s.property.date().type.clone(propertyValue); + expect(clonedPropertyValue).not.toBe(propertyValue); + expect(clonedPropertyValue).toEqual(propertyValue); + } + }); +}); diff --git a/tests/model/types/decimal.test.ts b/tests/model/types/decimal.test.ts new file mode 100644 index 0000000..d6549ca --- /dev/null +++ b/tests/model/types/decimal.test.ts @@ -0,0 +1,47 @@ +import {describe, expect, test} from "vitest"; +import {DecimalType, s} from "../../../src/library"; + +describe("decimal type", () => { + test("decimal type definition", () => { + const decimalType = s.property.decimal(); + expect(decimalType.type).toBeInstanceOf(DecimalType); + }); + + test("decimal type functions", () => { + expect(s.property.decimal().type.serialize(5.257)).toBe("5.257"); + expect(s.property.decimal().type.deserialize("5.257")).toBe(5.257); + expect(s.property.decimal().type.serializeDiff(542)).toBe("542"); + + expect(s.property.decimal().type.serialize(null)).toBe(null); + expect(s.property.decimal().type.deserialize(null)).toBe(null); + expect(s.property.decimal().type.serializeDiff(null)).toBe(null); + + expect(s.property.decimal().type.serialize(undefined)).toBe(undefined); + expect(s.property.decimal().type.deserialize(undefined)).toBe(undefined); + expect(s.property.decimal().type.serializeDiff(undefined)).toBe(undefined); + + expect(s.property.decimal().type.hasChanged(5.257, 5.257)).toBeFalsy(); + expect(s.property.decimal().type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.decimal().type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.decimal().type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.decimal().type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.decimal().type.hasChanged(null, 5.257)).toBeTruthy(); + expect(s.property.decimal().type.hasChanged(undefined, 5.257)).toBeTruthy(); + expect(s.property.decimal().type.hasChanged(5.257, null)).toBeTruthy(); + expect(s.property.decimal().type.hasChanged(5.257, undefined)).toBeTruthy(); + + expect(s.property.decimal().type.serializedHasChanged("5.257", "5.257")).toBeFalsy(); + expect(s.property.decimal().type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.decimal().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.decimal().type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.decimal().type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.decimal().type.serializedHasChanged(null, "5.257")).toBeTruthy(); + expect(s.property.decimal().type.serializedHasChanged(undefined, "5.257")).toBeTruthy(); + expect(s.property.decimal().type.serializedHasChanged("5.257", null)).toBeTruthy(); + expect(s.property.decimal().type.serializedHasChanged("5.257", undefined)).toBeTruthy(); + + s.property.decimal().type.resetDiff(5.257); + s.property.decimal().type.resetDiff(undefined); + s.property.decimal().type.resetDiff(null); + }); +}); diff --git a/tests/model/types/model.test.ts b/tests/model/types/model.test.ts new file mode 100644 index 0000000..45fb5d8 --- /dev/null +++ b/tests/model/types/model.test.ts @@ -0,0 +1,107 @@ +import {describe, expect, test} from "vitest"; +import {ModelType, s} from "../../../src/library"; + +class TestModel +{ + id: number; + name: string; + price: number; +} + +describe("model type", () => { + const testModel = s.defineModel({ + Class: TestModel, + properties: { + id: s.property.numeric(), + name: s.property.string(), + price: s.property.decimal(), + }, + identifier: "id", + }); + + test("model type definition", () => { + const modelType = s.property.model(testModel); + expect(modelType.type).toBeInstanceOf(ModelType); + }); + + test("model type functions", () => { + { // Try to serialize / deserialize. + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + expect(s.property.model(testModel).type.serialize(testModelInstance)).toEqual({ id: 1, name: "test", price: "12.548777" }); + expect(testModel.model( + s.property.model(testModel).type.deserialize({ id: 1, name: "test", price: "12.548777" }) + ).getInstanceProperties()).toEqual(testModel.model(testModelInstance).getInstanceProperties()); + } + + { // Try to serialize the difference. + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + testModelInstance.name = "new"; + expect(s.property.model(testModel).type.serializeDiff(testModelInstance)).toEqual({ id: 1, name: "new" }); + } + + expect(s.property.model(testModel).type.serialize(null)).toEqual(null); + expect(s.property.model(testModel).type.deserialize(null)).toEqual(null); + expect(s.property.model(testModel).type.serializeDiff(null)).toEqual(null); + + expect(s.property.model(testModel).type.serialize(undefined)).toEqual(undefined); + expect(s.property.model(testModel).type.deserialize(undefined)).toEqual(undefined); + expect(s.property.model(testModel).type.serializeDiff(undefined)).toEqual(undefined); + + { + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + expect(s.property.model(testModel).type.hasChanged(testModelInstance, testModelInstance)).toBeFalsy(); + } + { + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + testModelInstance.price = 12.548778; + expect(s.property.model(testModel).type.hasChanged(testModelInstance, testModelInstance)).toBeTruthy(); + } + expect(s.property.model(testModel).type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.model(testModel).type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.model(testModel).type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.model(testModel).type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.model(testModel).type.hasChanged(null, testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance)).toBeTruthy(); + expect(s.property.model(testModel).type.hasChanged(undefined, testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance)).toBeTruthy(); + expect(s.property.model(testModel).type.hasChanged(testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance, null)).toBeTruthy(); + expect(s.property.model(testModel).type.hasChanged(testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance, undefined)).toBeTruthy(); + + expect(s.property.model(testModel).type.serializedHasChanged( + { id: 1, name: "test", price: "12.548777" }, + { id: 1, price: "12.548777", name: "test" }, + )).toBeFalsy(); + expect(s.property.model(testModel).type.serializedHasChanged( + { id: 1, name: "test", price: "12.548777" }, + { id: 1, name: "test", price: "12.548778" }, + )).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.model(testModel).type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.model(testModel).type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged(null, { id: 1, name: "test", price: "12.548777" })).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged(undefined, { id: 1, name: "test", price: "12.548777" })).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged({ id: 1, name: "test", price: "12.548777" }, null)).toBeTruthy(); + expect(s.property.model(testModel).type.serializedHasChanged({ id: 1, name: "test", price: "12.548777" }, undefined)).toBeTruthy(); + + { // Serializing the difference to check that the difference has been reset. + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + testModelInstance.price = 555.555; + expect(testModel.model(testModelInstance).serializeDiff()).toEqual({ id: 1, price: "555.555" }); + s.property.model(testModel).type.resetDiff(testModelInstance); + expect(testModel.model(testModelInstance).serializeDiff()).toEqual({ id: 1 }); + } + + s.property.model(testModel).type.resetDiff(undefined); + s.property.model(testModel).type.resetDiff(null); + + { // Test that values are cloned in a different model instance. + const testModelInstance = testModel.model(Object.assign(new TestModel(), { id: 1, name: "test", price: 12.548777 })).instance; + testModelInstance.price = 555.555; + const clonedModelInstance = s.property.model(testModel).type.clone(testModelInstance); + expect(clonedModelInstance).not.toBe(testModelInstance); + expect(testModel.model(clonedModelInstance).getInstanceProperties()).toEqual(testModel.model(testModelInstance).getInstanceProperties()); + expect(testModel.model(clonedModelInstance).serializeDiff()).toEqual(testModel.model(testModelInstance).serializeDiff()); + } + expect(s.property.model(testModel).type.clone(undefined)).toBe(undefined); + expect(s.property.model(testModel).type.clone(null)).toBe(null); + }); +}); diff --git a/tests/model/types/numeric.test.ts b/tests/model/types/numeric.test.ts new file mode 100644 index 0000000..21c7d3f --- /dev/null +++ b/tests/model/types/numeric.test.ts @@ -0,0 +1,47 @@ +import {describe, expect, test} from "vitest"; +import {NumericType, s} from "../../../src/library"; + +describe("numeric type", () => { + test("numeric type definition", () => { + const numericType = s.property.numeric(); + expect(numericType.type).toBeInstanceOf(NumericType); + }); + + test("numeric type functions", () => { + expect(s.property.numeric().type.serialize(5.257)).toBe(5.257); + expect(s.property.numeric().type.deserialize(5.257)).toBe(5.257); + expect(s.property.numeric().type.serializeDiff(542)).toBe(542); + + expect(s.property.numeric().type.serialize(null)).toBe(null); + expect(s.property.numeric().type.deserialize(null)).toBe(null); + expect(s.property.numeric().type.serializeDiff(null)).toBe(null); + + expect(s.property.numeric().type.serialize(undefined)).toBe(undefined); + expect(s.property.numeric().type.deserialize(undefined)).toBe(undefined); + expect(s.property.numeric().type.serializeDiff(undefined)).toBe(undefined); + + expect(s.property.numeric().type.hasChanged(5.257, 5.257)).toBeFalsy(); + expect(s.property.numeric().type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.numeric().type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.numeric().type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.numeric().type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.numeric().type.hasChanged(null, 5.257)).toBeTruthy(); + expect(s.property.numeric().type.hasChanged(undefined, 5.257)).toBeTruthy(); + expect(s.property.numeric().type.hasChanged(5.257, null)).toBeTruthy(); + expect(s.property.numeric().type.hasChanged(5.257, undefined)).toBeTruthy(); + + expect(s.property.numeric().type.serializedHasChanged(5.257, 5.257)).toBeFalsy(); + expect(s.property.numeric().type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.numeric().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.numeric().type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.numeric().type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.numeric().type.serializedHasChanged(null, 5.257)).toBeTruthy(); + expect(s.property.numeric().type.serializedHasChanged(undefined, 5.257)).toBeTruthy(); + expect(s.property.numeric().type.serializedHasChanged(5.257, null)).toBeTruthy(); + expect(s.property.numeric().type.serializedHasChanged(5.257, undefined)).toBeTruthy(); + + s.property.numeric().type.resetDiff(5.257); + s.property.numeric().type.resetDiff(undefined); + s.property.numeric().type.resetDiff(null); + }); +}); diff --git a/tests/model/types/object.test.ts b/tests/model/types/object.test.ts new file mode 100644 index 0000000..2a63edf --- /dev/null +++ b/tests/model/types/object.test.ts @@ -0,0 +1,82 @@ +import {describe, expect, test} from "vitest"; +import {NumericType, ObjectType, s, StringType} from "../../../src/library"; + +describe("object type", () => { + test("object type definition", () => { + const objectType = s.property.object({ + test: s.property.string(), + another: s.property.numeric(), + }); + expect(objectType.type).toBeInstanceOf(ObjectType); + + expect((objectType.type as any).properties).toHaveLength(2); + for (const property of (objectType.type as any).properties) + { // Check all object properties. + if (property.name == "test") expect(property.definition.type).toBeInstanceOf(StringType); + else if (property.name == "another") expect(property.definition.type).toBeInstanceOf(NumericType); + else expect.unreachable(); + } + }); + + const testProperty = s.property.object({ + test: s.property.string(), + another: s.property.decimal(), + }); + + test("object type functions", () => { + expect(testProperty.type.serialize({ test: "test", another: 12.548777 })).toEqual({ test: "test", another: "12.548777" }); + expect(testProperty.type.deserialize({ test: "test", another: "12.548777" })).toEqual({ test: "test", another: 12.548777 }); + expect(testProperty.type.serializeDiff({ test: "test", another: 12.548777 })).toEqual({ test: "test", another: "12.548777" }); + + expect(testProperty.type.serialize(null)).toEqual(null); + expect(testProperty.type.deserialize(null)).toEqual(null); + expect(testProperty.type.serializeDiff(null)).toEqual(null); + + expect(testProperty.type.serialize(undefined)).toEqual(undefined); + expect(testProperty.type.deserialize(undefined)).toEqual(undefined); + expect(testProperty.type.serializeDiff(undefined)).toEqual(undefined); + + expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, { another: 12.548777, test: "test" })).toBeFalsy(); + expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, { test: "test", another: 12.548778 })).toBeTruthy(); + expect(testProperty.type.hasChanged(null, null)).toBeFalsy(); + expect(testProperty.type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(testProperty.type.hasChanged(null, undefined)).toBeTruthy(); + expect(testProperty.type.hasChanged(undefined, null)).toBeTruthy(); + expect(testProperty.type.hasChanged(null, { test: "test", another: 12.548777 })).toBeTruthy(); + expect(testProperty.type.hasChanged(undefined, { test: "test", another: 12.548777 })).toBeTruthy(); + expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, null)).toBeTruthy(); + expect(testProperty.type.hasChanged({ test: "test", another: 12.548777 }, undefined)).toBeTruthy(); + + expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, { another: "12.548777", test: "test" })).toBeFalsy(); + expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, { test: "test", another: "12.548778" })).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(null, null)).toBeFalsy(); + expect(testProperty.type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(testProperty.type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(null, { test: "test", another: "12.548777" })).toBeTruthy(); + expect(testProperty.type.serializedHasChanged(undefined, { test: "test", another: "12.548777" })).toBeTruthy(); + expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, null)).toBeTruthy(); + expect(testProperty.type.serializedHasChanged({ test: "test", another: "12.548777" }, undefined)).toBeTruthy(); + + testProperty.type.resetDiff({ test: "test", another: 12.548777 }); + testProperty.type.resetDiff(undefined); + testProperty.type.resetDiff(null); + + { // Test that values are cloned in a different object. + const propertyValue = { test: "test", another: 12.548777 }; + const clonedPropertyValue = testProperty.type.clone(propertyValue); + expect(clonedPropertyValue).not.toBe(propertyValue); + expect(clonedPropertyValue).toEqual(propertyValue); + } + { // Test that values are cloned in a different object. + const propertyValue = { arr: [12, 11] }; + const clonedPropertyValue = s.property.object({ arr: s.property.array(s.property.numeric()) }).type.clone(propertyValue); + expect(clonedPropertyValue).not.toBe(propertyValue); + expect(clonedPropertyValue).toEqual(propertyValue); + expect(clonedPropertyValue.arr).not.toBe(propertyValue.arr); + expect(clonedPropertyValue.arr).toEqual(propertyValue.arr); + } + expect(testProperty.type.clone(undefined)).toBe(undefined); + expect(testProperty.type.clone(null)).toBe(null); + }); +}); diff --git a/tests/model/types/string.test.ts b/tests/model/types/string.test.ts new file mode 100644 index 0000000..963ba30 --- /dev/null +++ b/tests/model/types/string.test.ts @@ -0,0 +1,47 @@ +import {describe, expect, test} from "vitest"; +import {s, StringType} from "../../../src/library"; + +describe("string type", () => { + test("string type definition", () => { + const stringType = s.property.string(); + expect(stringType.type).toBeInstanceOf(StringType); + }); + + test("string type functions", () => { + expect(s.property.string().type.serialize("test")).toBe("test"); + expect(s.property.string().type.deserialize("test")).toBe("test"); + expect(s.property.string().type.serializeDiff("test")).toBe("test"); + + expect(s.property.string().type.serialize(null)).toBe(null); + expect(s.property.string().type.deserialize(null)).toBe(null); + expect(s.property.string().type.serializeDiff(null)).toBe(null); + + expect(s.property.string().type.serialize(undefined)).toBe(undefined); + expect(s.property.string().type.deserialize(undefined)).toBe(undefined); + expect(s.property.string().type.serializeDiff(undefined)).toBe(undefined); + + expect(s.property.string().type.hasChanged("test", "test")).toBeFalsy(); + expect(s.property.string().type.hasChanged(null, null)).toBeFalsy(); + expect(s.property.string().type.hasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.string().type.hasChanged(null, undefined)).toBeTruthy(); + expect(s.property.string().type.hasChanged(undefined, null)).toBeTruthy(); + expect(s.property.string().type.hasChanged(null, "test")).toBeTruthy(); + expect(s.property.string().type.hasChanged(undefined, "test")).toBeTruthy(); + expect(s.property.string().type.hasChanged("test", null)).toBeTruthy(); + expect(s.property.string().type.hasChanged("test", undefined)).toBeTruthy(); + + expect(s.property.string().type.serializedHasChanged("test", "test")).toBeFalsy(); + expect(s.property.string().type.serializedHasChanged(null, null)).toBeFalsy(); + expect(s.property.string().type.serializedHasChanged(undefined, undefined)).toBeFalsy(); + expect(s.property.string().type.serializedHasChanged(null, undefined)).toBeTruthy(); + expect(s.property.string().type.serializedHasChanged(undefined, null)).toBeTruthy(); + expect(s.property.string().type.serializedHasChanged(null, "test")).toBeTruthy(); + expect(s.property.string().type.serializedHasChanged(undefined, "test")).toBeTruthy(); + expect(s.property.string().type.serializedHasChanged("test", null)).toBeTruthy(); + expect(s.property.string().type.serializedHasChanged("test", undefined)).toBeTruthy(); + + s.property.string().type.resetDiff("test"); + s.property.string().type.resetDiff(undefined); + s.property.string().type.resetDiff(null); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index 1544229..38bff29 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,13 +10,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { sourcemap: true, minify: "esbuild", lib: { - entry: "src/index.ts", + entry: "src/library.ts", formats: ["es"], fileName: "index", }, - rollupOptions: { - external: ["reflect-metadata"], - }, }, plugins: [