Rewrite model system, solve circular dependencies issues, better testing, clone function.
This commit is contained in:
parent
6eee1b709e
commit
f5502109ac
35 changed files with 1825 additions and 778 deletions
|
@ -1,10 +0,0 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
|
||||
roots: [
|
||||
"./tests",
|
||||
],
|
||||
};
|
24
package.json
24
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@sharkitek/core",
|
||||
"version": "3.3.0",
|
||||
"version": "4.0.0",
|
||||
"description": "TypeScript library for well-designed model architectures.",
|
||||
"keywords": [
|
||||
"deserialization",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"repository": "https://code.zeptotech.net/Sharkitek/Core",
|
||||
"author": {
|
||||
"name": "Madeorsk",
|
||||
"email": "madeorsk@protonmail.com"
|
||||
"email": "m@deor.sk"
|
||||
},
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
|
@ -24,24 +24,24 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"test": "jest"
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage"
|
||||
},
|
||||
"type": "module",
|
||||
"source": "src/index.ts",
|
||||
"source": "src/library.ts",
|
||||
"types": "lib/index.d.ts",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.7.4",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"@types/node": "^22.13.14",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-dts": "^4.2.2"
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"vite-plugin-dts": "^4.5.3",
|
||||
"vitest": "^3.0.9"
|
||||
},
|
||||
"packageManager": "yarn@4.5.0"
|
||||
"packageManager": "yarn@4.6.0"
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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";
|
|
@ -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());
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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";
|
|
@ -1,4 +0,0 @@
|
|||
import * as s from "./Model";
|
||||
export * from "./Model";
|
||||
export { s };
|
||||
export default s;
|
4
src/library.ts
Normal file
4
src/library.ts
Normal 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
16
src/model/index.ts
Normal 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
482
src/model/model.ts
Normal 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
10
src/model/properties.ts
Normal 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";
|
|
@ -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.
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
|
@ -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
51
src/model/types/date.ts
Normal 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());
|
||||
}
|
|
@ -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
116
src/model/types/model.ts
Normal 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;
|
||||
}
|
|
@ -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
159
src/model/types/object.ts
Normal 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));
|
||||
}
|
|
@ -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.
|
|
@ -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
4
src/utils.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Type definition of a class constructor.
|
||||
*/
|
||||
export type ConstructorOf<T extends object> = { new(): T; };
|
|
@ -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
329
tests/model.test.ts
Normal 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");
|
||||
});
|
||||
});
|
125
tests/model/types/array.test.ts
Normal file
125
tests/model/types/array.test.ts
Normal 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);
|
||||
});
|
||||
});
|
53
tests/model/types/boolean.test.ts
Normal file
53
tests/model/types/boolean.test.ts
Normal 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);
|
||||
});
|
||||
});
|
62
tests/model/types/date.test.ts
Normal file
62
tests/model/types/date.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
47
tests/model/types/decimal.test.ts
Normal file
47
tests/model/types/decimal.test.ts
Normal 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);
|
||||
});
|
||||
});
|
107
tests/model/types/model.test.ts
Normal file
107
tests/model/types/model.test.ts
Normal 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);
|
||||
});
|
||||
});
|
47
tests/model/types/numeric.test.ts
Normal file
47
tests/model/types/numeric.test.ts
Normal 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);
|
||||
});
|
||||
});
|
82
tests/model/types/object.test.ts
Normal file
82
tests/model/types/object.test.ts
Normal 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);
|
||||
});
|
||||
});
|
47
tests/model/types/string.test.ts
Normal file
47
tests/model/types/string.test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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: [
|
||||
|
|
Loading…
Add table
Reference in a new issue