Rewrite model system, solve circular dependencies issues, better testing, clone function.

This commit is contained in:
Madeorsk 2025-03-29 22:59:13 +01:00
parent 6eee1b709e
commit f5502109ac
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
35 changed files with 1825 additions and 778 deletions

View file

@ -1,10 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
export default {
preset: "ts-jest",
testEnvironment: "node",
roots: [
"./tests",
],
};

View file

@ -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"
}

View file

@ -1,296 +0,0 @@
import {Definition} from "./PropertyDefinition";
/**
* Type definition of a model constructor.
*/
export type ConstructorOf<T extends object> = { new(): T; };
/**
* Unknown property definition.
*/
export type UnknownDefinition = Definition<unknown, unknown>;
/**
* A model shape.
*/
export type ModelShape = Record<string, UnknownDefinition>;
/**
* Properties of a model based on its shape.
*/
export type PropertiesModel<Shape extends ModelShape> = {
[k in keyof Shape]: Shape[k]["_sharkitek"];
};
/**
* Serialized object type based on model shape.
*/
export type SerializedModel<Shape extends ModelShape> = {
[k in keyof Shape]: Shape[k]["_serialized"];
};
/**
* Type of a model object.
*/
export type Model<Shape extends ModelShape, IdentifierType = unknown> = ModelDefinition<Shape, IdentifierType> & PropertiesModel<Shape>;
/**
* Type of the extends function of model classes.
*/
export type ExtendsFunctionType<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any> =
<Extension extends object>(extension: ThisType<ModelType> & Extension) => ModelClass<ModelType & Extension, Shape, Identifier>;
/**
* Type of a model class.
*/
export type ModelClass<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any> = (
ConstructorOf<ModelType> & {
extends: ExtendsFunctionType<ModelType, Shape, Identifier>;
}
);
/**
* Identifier type.
*/
export type IdentifierType<Shape extends ModelShape, K extends keyof Shape> = Shape[K]["_sharkitek"];
/**
* Identifier name type.
*/
export type IdentifierNameType<Shape> = Shape extends ModelShape ? keyof Shape : unknown;
/**
* Interface of a Sharkitek model definition.
*/
export interface ModelDefinition<Shape extends ModelShape, IdentifierType, ModelType extends Model<Shape, IdentifierType> = Model<Shape, IdentifierType>>
{
/**
* Get model identifier.
*/
getIdentifier(): IdentifierType;
/**
* Get model identifier name.
*/
getIdentifierName(): IdentifierNameType<Shape>;
/**
* Serialize the model.
*/
serialize(): SerializedModel<Shape>;
/**
* Deserialize the model.
* @param obj Serialized object.
*/
deserialize(obj: SerializedModel<Shape>): 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<SerializedModel<Shape>>;
/**
* 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<SerializedModel<Shape>>;
}
/**
* Define a Sharkitek model.
* @param shape Model shape definition.
* @param identifier Identifier property name.
*/
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends IdentifierNameType<Shape> = any>(
shape: Shape,
identifier?: Identifier,
): ModelClass<ModelType, Shape, Identifier>
{
// Get shape entries.
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
return withExtends(
// Initialize generic model class.
class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>, 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<Shape>
);
}
/**
* Calling a function for each defined property.
* @param callback - The function to call.
* @protected
*/
protected forEachModelProperty<ReturnType>(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<PropertiesModel<Shape>> = {};
/**
* The original (serialized) object.
* @protected
*/
protected _originalObject: SerializedModel<Shape>|null = null;
getIdentifier(): IdentifierType<Shape, Identifier>
{
return (this as PropertiesModel<Shape>)?.[identifier];
}
getIdentifierName(): IdentifierNameType<Shape>
{
return identifier;
}
serialize(): SerializedModel<Shape>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel<Shape>)?.[propertyName]);
});
return serializedObject as SerializedModel<Shape>; // Returning the serialized object.
}
deserialize(obj: SerializedModel<Shape>): ModelType
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value.
(this as PropertiesModel<Shape>)[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<Shape>)[propertyName])
// There is a difference, we should return false.
? true
// There is no difference, returning nothing.
: undefined
)) === true;
}
serializeDiff(): Partial<SerializedModel<Shape>>
{
// Creating an empty (=> partial) serialized object.
const serializedObject: Partial<SerializedModel<Shape>> = {};
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<Shape>)[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<Shape>)?.[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<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
});
}
patch(): Partial<SerializedModel<Shape>>
{
// 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<ModelType>
);
}
/**
* Any Sharkitek model.
*/
export type AnyModel = Model<any, any>;
/**
* Any Sharkitek model class.
*/
export type AnyModelClass = ModelClass<AnyModel, any>;
/**
* Add extends function to a model class.
* @param genericModel The model class on which to add the extends function.
*/
function withExtends<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
genericModel: ConstructorOf<ModelType>
): ModelClass<ModelType, Shape, Identifier>
{
return Object.assign(
genericModel,
{ // Extends function definition.
extends<Extension extends object>(extension: Extension): ModelClass<ModelType & Extension, Shape, Identifier>
{
// Clone the model class and add extends function.
const classClone = withExtends(class extends (genericModel as AnyModelClass) {} as AnyModelClass as ConstructorOf<ModelType & Extension>);
// Add extension to the model class prototype.
Object.assign(classClone.prototype, extension);
return classClone;
}
}
) as AnyModelClass as ModelClass<ModelType, Shape, Identifier>;
}

View file

@ -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";

View file

@ -1,37 +0,0 @@
import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/**
* Type of dates.
*/
export class DateType extends Type<string, Date>
{
deserialize(value: string|null|undefined): Date|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
return new Date(value);
}
serialize(value: Date|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<string, Date>
{
return define(new DateType());
}

View file

@ -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<Shape extends ModelShape> extends Type<SerializedModel<Shape>, Model<Shape>>
{
/**
* Initialize a new model type of a Sharkitek model property.
* @param modelConstructor Model constructor.
*/
constructor(protected modelConstructor: ConstructorOf<Model<Shape>>)
{
super();
}
serialize(value: Model<Shape>|null|undefined): SerializedModel<Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing the given model.
return value?.serialize();
}
deserialize(value: SerializedModel<Shape>|null|undefined): Model<Shape>|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<Shape>;
}
serializeDiff(value: Model<Shape>|null|undefined): Partial<SerializedModel<Shape>>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing the given model.
return value?.serializeDiff();
}
resetDiff(value: Model<Shape>|null|undefined): void
{
// Reset diff of the given model.
value?.resetDiff();
}
propertyHasChanged(originalValue: Model<Shape>|null|undefined, currentValue: Model<Shape>|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<Shape extends ModelShape>(modelConstructor: ConstructorOf<Model<Shape>>): Definition<SerializedModel<Shape>, Model<Shape>>
{
return define(new ModelType(modelConstructor));
}

View file

@ -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<Shape extends ModelShape> extends Type<SerializedModel<Shape>, PropertiesModel<Shape>>
{
/**
* Initialize a new object type of a Sharkitek model property.
* @param shape
*/
constructor(protected readonly shape: Shape)
{
super();
}
deserialize(value: SerializedModel<Shape>|null|undefined): PropertiesModel<Shape>|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<Shape>;
}
serialize(value: PropertiesModel<Shape>|null|undefined): SerializedModel<Shape>|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<Shape>;
}
serializeDiff(value: PropertiesModel<Shape>|null|undefined): Partial<SerializedModel<Shape>>|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<Shape>;
}
resetDiff(value: PropertiesModel<Shape>|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<Shape>|null|undefined, currentValue: PropertiesModel<Shape>|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<Shape>|null|undefined, currentValue: SerializedModel<Shape>|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 extends ModelShape>(shape: Shape): Definition<SerializedModel<Shape>, PropertiesModel<Shape>>
{
return define(new ObjectType(shape));
}

View file

@ -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";

View file

@ -1,4 +0,0 @@
import * as s from "./Model";
export * from "./Model";
export { s };
export default s;

4
src/library.ts Normal file
View file

@ -0,0 +1,4 @@
import * as s from "./model";
export * from "./model";
export { s };
export default s;

16
src/model/index.ts Normal file
View file

@ -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";

482
src/model/model.ts Normal file
View file

@ -0,0 +1,482 @@
import {Definition, UnknownDefinition} from "./property-definition";
import {ConstructorOf} from "../utils";
/**
* A model shape.
*/
export type ModelShape<T extends object> = Partial<{
[k in keyof T]: Definition<unknown, T[k]>;
}>;
/**
* Properties values of a model based on its shape.
*/
export type ModelPropertiesValues<T extends object, Shape extends ModelShape<T>> = {
[k in keyof Shape]: Shape[k]["_sharkitek"];
};
/**
* Serialized object type based on model shape.
*/
export type SerializedModel<T extends object, Shape extends ModelShape<T>> = {
[k in keyof Shape]?: Shape[k]["_serialized"];
};
/**
* This is an experimental serialized model type declaration.
* @deprecated
*/
type ExperimentalSerializedModel<T extends object, Shape extends ModelShape<T>>
= Omit<ExperimentalSerializedModelBase<T, Shape>, ExperimentalSerializedModelOptionalKeys<T, Shape>>
& Pick<Partial<ExperimentalSerializedModelBase<T, Shape>>, ExperimentalSerializedModelOptionalKeys<T, Shape>>;
type ExperimentalSerializedModelBase<T extends object, Shape extends ModelShape<T>> = {
[k in keyof Shape]: Shape[k]["_serialized"];
};
type ExperimentalSerializedModelOptionalKeys<T extends object, Shape extends ModelShape<T>> = {
[k in keyof Shape]: Shape[k]["_serialized"] extends undefined ? k : never
}[keyof Shape];
/**
* A sharkitek model instance, with internal model state.
*/
export type ModelInstance<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> = T & {
/**
* The Sharkitek model state.
*/
_sharkitek: Model<T, Shape, Identifier>;
};
/**
* Identifier definition type.
*/
export type IdentifierDefinition<T extends object, Shape extends ModelShape<T>> = (keyof Shape)|((keyof Shape)[]);
/**
* Identifier type.
*/
export type IdentifierType<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
= 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<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
{
/**
* Model class.
*/
Class: ConstructorOf<T>;
/**
* 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<T extends object, Shape extends ModelShape<T>>
{
/**
* 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<T extends object, Shape extends ModelShape<T>> = ModelProperty<T, Shape>[];
/**
* A Sharkitek model state.
*/
export class Model<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
{
/**
* The model manager instance.
*/
readonly manager: ModelManager<T, Shape, Identifier>;
/**
* The model definition object.
*/
readonly definition: ModelDefinition<T, Shape, Identifier>;
/**
* Iterable properties.
*/
readonly properties: ModelProperties<T, Shape>;
/**
* The actual instance of the model.
*/
instance: ModelInstance<T, Shape, Identifier>;
/**
* Original values, to keep track of the changes on the model properties.
* @protected
*/
protected original: {
/**
* The original properties values.
*/
properties: Partial<ModelPropertiesValues<T, Shape>>;
/**
* The original serialized object, if there is one.
*/
serialized: SerializedModel<T, Shape>|null;
};
/**
* Initialize a new model state with the defined properties.
* @param manager The model manager.
*/
constructor(manager: ModelManager<T, Shape, Identifier>)
{
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<T, Shape, Identifier>;
// 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<T, Shape>): 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<T, Shape, Identifier>
{
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<T, Shape, Identifier>;
}
else
{ // The identifier is a simple property, get its value.
return this.instance?.[this.definition.identifier as keyof Shape as keyof T] as IdentifierType<T, Shape, Identifier>;
}
}
/**
* Get current model instance properties.
*/
getInstanceProperties(): ModelPropertiesValues<T, Shape>
{
// Initialize an empty model properties object.
const instanceProperties: Partial<ModelPropertiesValues<T, Shape>> = {};
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<T, Shape>; // Returning the properties object.
}
/**
* Serialize the model instance.
*/
serialize(): SerializedModel<T, Shape>
{
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
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<T, Shape>; // 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<SerializedModel<T, Shape>>
{
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
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<SerializedModel<T, Shape>>
{
// 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<T, Shape, Identifier>
{
// 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<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
{
/**
* Defined properties.
*/
properties: ModelProperties<T, Shape>;
/**
* Initialize a model manager from a model definition.
* @param definition The model definition.
*/
constructor(public readonly definition: ModelDefinition<T, Shape, Identifier>)
{
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<T, Shape>);
}
}
/**
* 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<T, Shape, Identifier>|null = null): Model<T, Shape, Identifier>
{ // 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<T, Shape, Identifier>)?._sharkitek ?? (new Model<T, Shape, Identifier>(this))).fromInstance(instance);
else
// No instance, initialize a new one.
return (new Model<T, Shape, Identifier>(this)).initInstance();
}
/**
* Parse the serialized model object to a new model instance.
* @param serialized The serialized model object.
*/
parse(serialized: SerializedModel<T, Shape>): ModelInstance<T, Shape, Identifier>
{
return this.model().deserialize(serialized).instance;
}
}
/**
* Define a new model.
* @param definition The model definition object.
*/
export function defineModel<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>(
definition: ModelDefinition<T, Shape, Identifier>
)
{
return new ModelManager<T, Shape, Identifier>(definition);
}
/**
* A generic model manager for a provided model type, to use in circular dependencies.
*/
export type GenericModelManager<T extends object> = ModelManager<T, ModelShape<T>, IdentifierDefinition<T, ModelShape<T>>>;
/**
* Function to get a model manager lazily.
*/
export type LazyModelManager<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
= (() => ModelManager<T, Shape, Identifier>);
/**
* A model manager definition that can be lazy.
*/
export type DeclaredModelManager<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>
= ModelManager<T, Shape, Identifier>|LazyModelManager<T, Shape, Identifier>;

10
src/model/properties.ts Normal file
View file

@ -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";

View file

@ -1,4 +1,4 @@
import {Type} from "./Types/Type";
import {Type} from "./types/type";
/**
* Property definition class.
@ -16,6 +16,16 @@ export class Definition<SerializedType, ModelType>
{}
}
/**
* Unknown property definition.
*/
export type UnknownDefinition = Definition<unknown, unknown>;
/**
* Any property definition.
*/
export type AnyDefinition = Definition<any, any>;
/**
* New definition of a property of the given type.
* @param type Type of the property to define.

View file

@ -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<SerializedValueType, SharkitekValueType> extends Type<Ser
value.forEach((value) => 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<SerializedValueType, SharkitekValueType> extends Type<Ser
return false; // No change detected.
}
serializedPropertyHasChanged(originalValue: SerializedValueType[] | null | undefined, currentValue: SerializedValueType[] | null | undefined): boolean
serializedHasChanged(originalValue: SerializedValueType[] | null | undefined, currentValue: SerializedValueType[] | 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.serializedHasChanged(originalValue, currentValue);
for (const key of originalValue.keys())
{ // Check for any change for each value in the array.
if (this.valueDefinition.type.serializedPropertyHasChanged(originalValue[key], currentValue[key]))
if (this.valueDefinition.type.serializedHasChanged(originalValue[key], currentValue[key]))
// The value has changed, the array is different.
return true;
}
return false; // No change detected.
}
clone<T extends SharkitekValueType[]>(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.
}
}
/**

View file

@ -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<boolean, boolean>
export class BooleanType extends Type<boolean, boolean>
{
deserialize(value: boolean|null|undefined): boolean|null|undefined
{
@ -28,15 +28,15 @@ export class BoolType extends Type<boolean, boolean>
/**
* New boolean property definition.
*/
export function bool(): Definition<boolean, boolean>
export function boolean(): Definition<boolean, boolean>
{
return define(new BoolType());
return define(new BooleanType());
}
/**
* New boolean property definition.
* Alias of bool.
* Alias of boolean.
*/
export function boolean(): ReturnType<typeof bool>
export function bool(): ReturnType<typeof boolean>
{
return bool();
return boolean();
}

51
src/model/types/date.ts Normal file
View file

@ -0,0 +1,51 @@
import {Type} from "./type";
import {define, Definition} from "../property-definition";
/**
* Type of dates.
*/
export class DateType extends Type<string, Date>
{
deserialize(value: string|null|undefined): Date|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
return new Date(value);
}
serialize(value: Date|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<string, Date>
{
return define(new DateType());
}

View file

@ -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.

116
src/model/types/model.ts Normal file
View file

@ -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<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>> extends Type<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>>
{
/**
* Initialize a new model type of a Sharkitek model property.
* @param declaredModelManager Model manager.
*/
constructor(protected declaredModelManager: DeclaredModelManager<T, Shape, Identifier>)
{
super();
}
/**
* Resolve the defined model using the declared model, that can be defined lazily.
*/
get definedModel(): ModelManager<T, Shape, Identifier>
{
return typeof this.declaredModelManager == "object" ? this.declaredModelManager : this.declaredModelManager();
}
serialize(value: ModelInstance<T, Shape, Identifier>|null|undefined): SerializedModel<T, Shape>|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<T, Shape>|null|undefined): ModelInstance<T, Shape, Identifier>|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<T, Shape, Identifier>|null|undefined): Partial<SerializedModel<T, Shape>>|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<T, Shape, Identifier>|null|undefined): void
{
// Reset diff of the given model.
this.definedModel.model(value).resetDiff();
}
hasChanged(originalValue: ModelInstance<T, Shape, Identifier>|null|undefined, currentValue: ModelInstance<T, Shape, Identifier>|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<T, Shape> | null | undefined, currentValue: SerializedModel<T, Shape> | 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<Type extends ModelInstance<T, Shape, Identifier>>(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<T extends object, Shape extends ModelShape<T>, Identifier extends IdentifierDefinition<T, Shape>>(
definedModel: DeclaredModelManager<T, Shape, Identifier>
): Definition<SerializedModel<T, Shape>, ModelInstance<T, Shape, Identifier>>
{
return define(new ModelType(definedModel));
}
export function circular<T extends object>(definedModel: () => any): () => GenericModelManager<T>
{
return definedModel;
}

View file

@ -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.

159
src/model/types/object.ts Normal file
View file

@ -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<Shape extends ModelShape<T>, T extends object> extends Type<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>>
{
/**
* Defined properties.
*/
properties: ModelProperties<T, Shape>;
/**
* 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<T, Shape>);
}
}
deserialize(value: SerializedModel<T, Shape>|null|undefined): ModelPropertiesValues<T, Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Initialize an empty object.
const obj: Partial<ModelPropertiesValues<T, Shape>> = {};
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<T, Shape>; // Returning serialized object.
}
serialize(value: ModelPropertiesValues<T, Shape>|null|undefined): SerializedModel<T, Shape>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
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<T, Shape>; // Returning the serialized object.
}
serializeDiff(value: ModelPropertiesValues<T, Shape>|null|undefined): Partial<SerializedModel<T, Shape>>|null|undefined
{
if (value === undefined) return undefined;
if (value === null) return null;
// Creating an empty serialized object.
const serializedObject: Partial<SerializedModel<T, Shape>> = {};
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<T, Shape>; // Returning the serialized object.
}
resetDiff(value: ModelPropertiesValues<T, Shape>|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<T, Shape>|null|undefined, currentValue: ModelPropertiesValues<T, Shape>|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<T, Shape>|null|undefined, currentValue: SerializedModel<T, Shape>|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<Type extends ModelPropertiesValues<T, Shape>>(value: Type|null|undefined): Type
{
// Handle NULL / undefined object.
if (!value) return super.clone(value);
// Initialize an empty object.
const cloned: Partial<ModelPropertiesValues<T, Shape>> = {};
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<Shape extends ModelShape<T>, T extends object>(shape: Shape): Definition<SerializedModel<T, Shape>, ModelPropertiesValues<T, Shape>>
{
return define(new ObjectType(shape));
}

View file

@ -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.

View file

@ -11,13 +11,13 @@ export abstract class Type<SerializedType, ModelType>
/**
* 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<SerializedType>|null|undefined
{
@ -26,7 +26,7 @@ export abstract class Type<SerializedType, ModelType>
/**
* 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<SerializedType, ModelType>
}
/**
* 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<T extends ModelType>(value: T|null|undefined): T
{
return structuredClone(value);
}
}

4
src/utils.ts Normal file
View file

@ -0,0 +1,4 @@
/**
* Type definition of a class constructor.
*/
export type ConstructorOf<T extends object> = { new(): T; };

View file

@ -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");
});

329
tests/model.test.ts Normal file
View file

@ -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>(() => 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");
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
}
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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: [