Sharkitek base: model class and basic types dans behaviors.
This commit is contained in:
parent
9d72c909cb
commit
ff1d55da78
13 changed files with 706 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# IDEA
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# JS library
|
||||||
|
|
||||||
|
coverage/
|
||||||
|
lib/
|
||||||
|
.parcel-cache/
|
||||||
|
.yarn/
|
||||||
|
.yarnrc*
|
||||||
|
yarn-error.log
|
||||||
|
.pnp*
|
||||||
|
|
||||||
|
yarn.lock
|
9
jest.config.js
Normal file
9
jest.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "node",
|
||||||
|
|
||||||
|
roots: [
|
||||||
|
"./tests",
|
||||||
|
],
|
||||||
|
};
|
29
package.json
Normal file
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Sharkitek core models library.",
|
||||||
|
"repository": "https://git.madeorsk.com/Sharkitek/core",
|
||||||
|
"author": "Madeorsk <madeorsk@protonmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "parcel build",
|
||||||
|
"dev": "parcel watch",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"reflect-metadata": "^0.1.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@parcel/packager-ts": "2.6.2",
|
||||||
|
"@parcel/transformer-typescript-types": "2.6.2",
|
||||||
|
"@types/jest": "^28.1.6",
|
||||||
|
"jest": "^28.1.3",
|
||||||
|
"parcel": "^2.6.2",
|
||||||
|
"ts-jest": "^28.0.7",
|
||||||
|
"typescript": "^4.7.4"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.2.2"
|
||||||
|
}
|
246
src/Model/Model.ts
Normal file
246
src/Model/Model.ts
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
import {Type} from "./Types/Type";
|
||||||
|
import "reflect-metadata";
|
||||||
|
import {ConstructorOf} from "./Types/ModelType";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key of Sharkitek property metadata.
|
||||||
|
*/
|
||||||
|
const sharkitekMetadataKey = Symbol("sharkitek");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key of Sharkitek model identifier.
|
||||||
|
*/
|
||||||
|
const modelIdentifierMetadataKey = Symbol("modelIdentifier");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sharkitek property metadata interface.
|
||||||
|
*/
|
||||||
|
interface SharkitekMetadataInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Property type instance.
|
||||||
|
*/
|
||||||
|
type: Type<any, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property decorator to define a Sharkitek model identifier.
|
||||||
|
*/
|
||||||
|
export function Identifier(obj: Model, propertyName: string): void
|
||||||
|
{
|
||||||
|
// Register the current property as identifier of the current model object.
|
||||||
|
Reflect.defineMetadata(modelIdentifierMetadataKey, propertyName, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property decorator for Sharkitek models properties.
|
||||||
|
* @param type - Type of the property.
|
||||||
|
*/
|
||||||
|
export function Property<SerializedType, SharkitekType>(type: Type<SerializedType, SharkitekType>): PropertyDecorator
|
||||||
|
{
|
||||||
|
// Return the decorator function.
|
||||||
|
return (obj: ConstructorOf<Model>, propertyName) => {
|
||||||
|
// Initializing property metadata.
|
||||||
|
const metadata: SharkitekMetadataInterface = {
|
||||||
|
type: type,
|
||||||
|
};
|
||||||
|
// Set property metadata.
|
||||||
|
Reflect.defineMetadata(sharkitekMetadataKey, metadata, obj, propertyName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Sharkitek model.
|
||||||
|
*/
|
||||||
|
export abstract class Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the Sharkitek model identifier.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getModelIdentifier(): string
|
||||||
|
{
|
||||||
|
return Reflect.getMetadata(modelIdentifierMetadataKey, this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the Sharkitek metadata of the property.
|
||||||
|
* @param propertyName - The name of the property for which to get metadata.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getPropertyMetadata(propertyName: string): SharkitekMetadataInterface
|
||||||
|
{
|
||||||
|
return Reflect.getMetadata(sharkitekMetadataKey, this, propertyName);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calling a function for a defined property.
|
||||||
|
* @param propertyName - The property for which to check definition.
|
||||||
|
* @param callback - The function called when the property is defined.
|
||||||
|
* @param notProperty - The function called when the property is not defined.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected propertyWithMetadata(propertyName: string, callback: (propertyMetadata: SharkitekMetadataInterface) => void, notProperty: () => void = () => {}): unknown
|
||||||
|
{
|
||||||
|
// Getting the current property metadata.
|
||||||
|
const propertyMetadata = this.getPropertyMetadata(propertyName);
|
||||||
|
if (propertyMetadata)
|
||||||
|
// Metadata are defined, calling the right callback.
|
||||||
|
return callback(propertyMetadata);
|
||||||
|
else
|
||||||
|
// Metadata are not defined, calling the right callback.
|
||||||
|
return notProperty();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Calling a function for each defined property.
|
||||||
|
* @param callback - The function to call.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected forEachModelProperty(callback: (propertyName: string, propertyMetadata: SharkitekMetadataInterface) => unknown): any|void
|
||||||
|
{
|
||||||
|
for (const propertyName of Object.keys(this))
|
||||||
|
{ // For each property, checking that its type is defined and calling the callback with its type.
|
||||||
|
const result = this.propertyWithMetadata(propertyName, (propertyMetadata) => {
|
||||||
|
// If the property is defined, calling the function with the property name and metadata.
|
||||||
|
const result = callback(propertyName, propertyMetadata);
|
||||||
|
|
||||||
|
// If there is a return value, returning it directly (loop is broken).
|
||||||
|
if (typeof result !== "undefined") return result;
|
||||||
|
|
||||||
|
// Update metadata if they have changed.
|
||||||
|
Reflect.defineMetadata(sharkitekMetadataKey, propertyMetadata, this, propertyName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is a return value, returning it directly (loop is broken).
|
||||||
|
if (typeof result !== "undefined") return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original properties values.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _originalProperties: Record<string, any> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original (serialized) object.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected _originalObject: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the model is new or not.
|
||||||
|
*/
|
||||||
|
isNew(): boolean
|
||||||
|
{
|
||||||
|
return !this._originalObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the model is dirty or not.
|
||||||
|
*/
|
||||||
|
isDirty(): boolean
|
||||||
|
{
|
||||||
|
return this.forEachModelProperty((propertyName, propertyMetadata) => (
|
||||||
|
// For each property, checking if it is different.
|
||||||
|
propertyMetadata.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])
|
||||||
|
// There is a difference, we should return false.
|
||||||
|
? true
|
||||||
|
// There is not difference, returning nothing.
|
||||||
|
: undefined
|
||||||
|
)) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model identifier.
|
||||||
|
*/
|
||||||
|
getIdentifier(): unknown
|
||||||
|
{
|
||||||
|
return (this as any)[this.getModelIdentifier()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set current properties values as original values.
|
||||||
|
*/
|
||||||
|
resetDiff()
|
||||||
|
{
|
||||||
|
this.forEachModelProperty((propertyName, propertyMetadata) => {
|
||||||
|
// For each property, set its original value to its current property value.
|
||||||
|
this._originalProperties[propertyName] = (this as any)[propertyName];
|
||||||
|
propertyMetadata.type.resetDiff((this as any)[propertyName]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Serialize the difference between current model state and original one.
|
||||||
|
*/
|
||||||
|
serializeDiff(): any
|
||||||
|
{
|
||||||
|
// Creating a serialized object.
|
||||||
|
const serializedDiff: any = {};
|
||||||
|
|
||||||
|
this.forEachModelProperty((propertyName, propertyMetadata) => {
|
||||||
|
// For each defined model property, adding it to the serialized object if it has changed.
|
||||||
|
if (this.getModelIdentifier() == propertyName
|
||||||
|
|| propertyMetadata.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]))
|
||||||
|
// Adding the current property to the serialized object if it is the identifier or its value has changed.
|
||||||
|
serializedDiff[propertyName] = propertyMetadata.type.serializeDiff((this as any)[propertyName]);
|
||||||
|
})
|
||||||
|
|
||||||
|
return serializedDiff; // Returning the serialized object.
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get difference between original values and current ones, then reset it.
|
||||||
|
* Similar to call `serializeDiff()` then `resetDiff()`.
|
||||||
|
*/
|
||||||
|
save(): any
|
||||||
|
{
|
||||||
|
// Get the difference.
|
||||||
|
const diff = this.serializeDiff();
|
||||||
|
|
||||||
|
// Once the difference has been gotten, reset it.
|
||||||
|
this.resetDiff();
|
||||||
|
|
||||||
|
return diff; // Return the difference.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the model.
|
||||||
|
*/
|
||||||
|
serialize(): void
|
||||||
|
{
|
||||||
|
// Creating a serialized object.
|
||||||
|
const serializedObject: any = {};
|
||||||
|
|
||||||
|
this.forEachModelProperty((propertyName, propertyMetadata) => {
|
||||||
|
// For each defined model property, adding it to the serialized object.
|
||||||
|
serializedObject[propertyName] = propertyMetadata.type.serialize((this as any)[propertyName]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return serializedObject; // Returning the serialized object.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special operations on parse.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected parse(): void
|
||||||
|
{} // Nothing by default. TODO: create a event system to create functions like "beforeDeserialization" or "afterDeserialization".
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the model.
|
||||||
|
*/
|
||||||
|
deserialize(serializedObject: any): this
|
||||||
|
{
|
||||||
|
this.forEachModelProperty((propertyName, propertyMetadata) => {
|
||||||
|
// For each defined model property, assigning its deserialized value to the model.
|
||||||
|
(this as any)[propertyName] = propertyMetadata.type.deserialize(serializedObject[propertyName]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset original property values.
|
||||||
|
this.resetDiff();
|
||||||
|
|
||||||
|
this._originalObject = serializedObject; // The model is not a new one, but loaded from a deserialized one.
|
||||||
|
|
||||||
|
return this; // Returning this, after deserialization.
|
||||||
|
}
|
||||||
|
}
|
53
src/Model/Types/ArrayType.ts
Normal file
53
src/Model/Types/ArrayType.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import {Type} from "./Type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of an array of values.
|
||||||
|
*/
|
||||||
|
export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<SerializedValueType[], SharkitekValueType[]>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs a new array type of Sharkitek model property.
|
||||||
|
* @param valueType - Type of the array values.
|
||||||
|
*/
|
||||||
|
constructor(protected valueType: Type<SerializedValueType, SharkitekValueType>)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(value: SharkitekValueType[]): SerializedValueType[]
|
||||||
|
{
|
||||||
|
return value.map((value) => (
|
||||||
|
// Serializing each value of the array.
|
||||||
|
this.valueType.serialize(value)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(value: SerializedValueType[]): SharkitekValueType[]
|
||||||
|
{
|
||||||
|
return value.map((serializedValue) => (
|
||||||
|
// Deserializing each value of the array.
|
||||||
|
this.valueType.deserialize(serializedValue)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeDiff(value: SharkitekValueType[]): any
|
||||||
|
{
|
||||||
|
// Serializing diff of all elements.
|
||||||
|
return value.map((value) => this.valueType.serializeDiff(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDiff(value: SharkitekValueType[]): void
|
||||||
|
{
|
||||||
|
// Reset diff of all elements.
|
||||||
|
value.forEach((value) => this.valueType.resetDiff(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of an array of values.
|
||||||
|
* @param valueType - Type of the array values.
|
||||||
|
*/
|
||||||
|
export function SArray<SerializedValueType, SharkitekValueType>(valueType: Type<SerializedValueType, SharkitekValueType>)
|
||||||
|
{
|
||||||
|
return new ArrayType<SerializedValueType, SharkitekValueType>(valueType);
|
||||||
|
}
|
22
src/Model/Types/DecimalType.ts
Normal file
22
src/Model/Types/DecimalType.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {Type} from "./Type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of decimal numbers.
|
||||||
|
*/
|
||||||
|
export class DecimalType extends Type<string, number>
|
||||||
|
{
|
||||||
|
deserialize(value: string): number
|
||||||
|
{
|
||||||
|
return parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(value: number): string
|
||||||
|
{
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of decimal numbers;
|
||||||
|
*/
|
||||||
|
export const SDecimal = new DecimalType();
|
55
src/Model/Types/ModelType.ts
Normal file
55
src/Model/Types/ModelType.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import {Type} from "./Type";
|
||||||
|
import {Model} from "../Model";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition of the constructor of a specific type.
|
||||||
|
*/
|
||||||
|
export type ConstructorOf<T> = { new(): T; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of a Sharkitek model value.
|
||||||
|
*/
|
||||||
|
export class ModelType<M extends Model> extends Type<any, M>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs a new model type of a Sharkitek model property.
|
||||||
|
* @param modelConstructor - Constructor of the model.
|
||||||
|
*/
|
||||||
|
constructor(protected modelConstructor: ConstructorOf<M>)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(value: M): any
|
||||||
|
{
|
||||||
|
// Serializing the given model.
|
||||||
|
return value.serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(value: any): M
|
||||||
|
{
|
||||||
|
// Deserializing the given object in the new model.
|
||||||
|
return (new this.modelConstructor()).deserialize(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeDiff(value: M): any
|
||||||
|
{
|
||||||
|
// Serializing the given model.
|
||||||
|
return value.serializeDiff();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDiff(value: M): void
|
||||||
|
{
|
||||||
|
// Reset diff of the given model.
|
||||||
|
value.resetDiff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of a Sharkitek model value.
|
||||||
|
* @param modelConstructor - Constructor of the model.
|
||||||
|
*/
|
||||||
|
export function SModel<M extends Model>(modelConstructor: ConstructorOf<M>)
|
||||||
|
{
|
||||||
|
return new ModelType(modelConstructor);
|
||||||
|
}
|
22
src/Model/Types/NumericType.ts
Normal file
22
src/Model/Types/NumericType.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {Type} from "./Type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of any numeric value.
|
||||||
|
*/
|
||||||
|
export class NumericType extends Type<number, number>
|
||||||
|
{
|
||||||
|
deserialize(value: number): number
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(value: number): number
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of any numeric value.
|
||||||
|
*/
|
||||||
|
export const SNumeric = new NumericType();
|
22
src/Model/Types/StringType.ts
Normal file
22
src/Model/Types/StringType.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {Type} from "./Type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of any string value.
|
||||||
|
*/
|
||||||
|
export class StringType extends Type<string, string>
|
||||||
|
{
|
||||||
|
deserialize(value: string): string
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(value: string): string
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of any string value.
|
||||||
|
*/
|
||||||
|
export const SString = new StringType();
|
55
src/Model/Types/Type.ts
Normal file
55
src/Model/Types/Type.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Abstract class of a Sharkitek model property type.
|
||||||
|
*/
|
||||||
|
export abstract class Type<SerializedType, SharkitekType>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Serialize the given value of a Sharkitek model property.
|
||||||
|
* @param value - Value to serialize.
|
||||||
|
*/
|
||||||
|
abstract serialize(value: SharkitekType): SerializedType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the given value of a serialized Sharkitek model.
|
||||||
|
* @param value - Value to deserialize.
|
||||||
|
*/
|
||||||
|
abstract deserialize(value: SerializedType): SharkitekType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given value only if it has changed.
|
||||||
|
* @param value - Value to deserialize.
|
||||||
|
*/
|
||||||
|
serializeDiff(value: SharkitekType): SerializedType|null
|
||||||
|
{
|
||||||
|
return this.serialize(value); // By default, nothing changes.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the difference between the original value and the current one.
|
||||||
|
* @param value - Value for which reset diff data.
|
||||||
|
*/
|
||||||
|
resetDiff(value: SharkitekType): void
|
||||||
|
{
|
||||||
|
// By default, nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the property value has changed.
|
||||||
|
* @param originalValue - Original property value.
|
||||||
|
* @param currentValue - Current property value.
|
||||||
|
*/
|
||||||
|
propertyHasChanged(originalValue: SharkitekType, currentValue: SharkitekType): boolean
|
||||||
|
{
|
||||||
|
return originalValue != currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the serialized property value has changed.
|
||||||
|
* @param originalValue - Original serialized property value.
|
||||||
|
* @param currentValue - Current serialized property value.
|
||||||
|
*/
|
||||||
|
serializedPropertyHasChanged(originalValue: SerializedType, currentValue: SerializedType): boolean
|
||||||
|
{
|
||||||
|
return originalValue != currentValue;
|
||||||
|
}
|
||||||
|
}
|
10
src/index.ts
Normal file
10
src/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
|
||||||
|
export * from "./Model/Model";
|
||||||
|
|
||||||
|
export * from "./Model/Types/Type";
|
||||||
|
export * from "./Model/Types/ArrayType";
|
||||||
|
export * from "./Model/Types/DecimalType";
|
||||||
|
export * from "./Model/Types/ModelType";
|
||||||
|
export * from "./Model/Types/NumericType";
|
||||||
|
export * from "./Model/Types/StringType";
|
146
tests/Model.test.ts
Normal file
146
tests/Model.test.ts
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import {SArray, SDecimal, SModel, SNumeric, SString, Identifier, Model, Property} from "../src";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Another test model.
|
||||||
|
*/
|
||||||
|
class Author extends Model
|
||||||
|
{
|
||||||
|
@Property(SString)
|
||||||
|
name: string = undefined;
|
||||||
|
|
||||||
|
@Property(SString)
|
||||||
|
firstName: string = undefined;
|
||||||
|
|
||||||
|
@Property(SString)
|
||||||
|
email: string = undefined;
|
||||||
|
|
||||||
|
constructor(name: string = undefined, firstName: string = undefined, email: string = undefined)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test model.
|
||||||
|
*/
|
||||||
|
class Article extends Model
|
||||||
|
{
|
||||||
|
@Property(SNumeric)
|
||||||
|
@Identifier
|
||||||
|
id: number = undefined;
|
||||||
|
|
||||||
|
@Property(SString)
|
||||||
|
title: string = undefined;
|
||||||
|
|
||||||
|
@Property(SArray(SModel(Author)))
|
||||||
|
authors: Author[] = [];
|
||||||
|
|
||||||
|
@Property(SString)
|
||||||
|
text: string = undefined;
|
||||||
|
|
||||||
|
@Property(SDecimal)
|
||||||
|
evaluation: number = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("deserialize", () => {
|
||||||
|
expect((new Article()).deserialize({
|
||||||
|
id: 1,
|
||||||
|
title: "this is a test",
|
||||||
|
authors: [
|
||||||
|
{ name: "DOE", firstName: "John", email: "test@test.test" },
|
||||||
|
{ name: "TEST", firstName: "Another", email: "another@test.test" },
|
||||||
|
],
|
||||||
|
text: "this is a long test.",
|
||||||
|
evaluation: "25.23",
|
||||||
|
}).serialize()).toStrictEqual({
|
||||||
|
id: 1,
|
||||||
|
title: "this is a test",
|
||||||
|
authors: [
|
||||||
|
{ name: "DOE", firstName: "John", email: "test@test.test" },
|
||||||
|
{ name: "TEST", firstName: "Another", email: "another@test.test" },
|
||||||
|
],
|
||||||
|
text: "this is a long test.",
|
||||||
|
evaluation: "25.23",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("create and check state then serialize", () => {
|
||||||
|
const article = new Article();
|
||||||
|
article.id = 1;
|
||||||
|
article.title = "this is a test";
|
||||||
|
article.authors = [
|
||||||
|
new Author("DOE", "John", "test@test.test"),
|
||||||
|
];
|
||||||
|
article.text = "this is a long test.";
|
||||||
|
article.evaluation = 25.23;
|
||||||
|
|
||||||
|
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" },
|
||||||
|
],
|
||||||
|
text: "this is a long test.",
|
||||||
|
evaluation: "25.23",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("deserialize then save", () => {
|
||||||
|
const article = (new Article()).deserialize({
|
||||||
|
id: 1,
|
||||||
|
title: "this is a test",
|
||||||
|
authors: [
|
||||||
|
{ name: "DOE", firstName: "John", email: "test@test.test" },
|
||||||
|
{ name: "TEST", firstName: "Another", email: "another@test.test" },
|
||||||
|
],
|
||||||
|
text: "this is a long test.",
|
||||||
|
evaluation: "25.23",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(article.isNew()).toBeFalsy();
|
||||||
|
expect(article.isDirty()).toBeFalsy();
|
||||||
|
expect(article.evaluation).toStrictEqual(25.23);
|
||||||
|
|
||||||
|
article.text = "Modified text.";
|
||||||
|
|
||||||
|
expect(article.isDirty()).toBeTruthy();
|
||||||
|
|
||||||
|
expect(article.save()).toStrictEqual({
|
||||||
|
id: 1,
|
||||||
|
text: "Modified text.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("save with modified submodels", () => {
|
||||||
|
const article = (new Article()).deserialize({
|
||||||
|
id: 1,
|
||||||
|
title: "this is a test",
|
||||||
|
authors: [
|
||||||
|
{ name: "DOE", firstName: "John", email: "test@test.test" },
|
||||||
|
{ name: "TEST", firstName: "Another", email: "another@test.test" },
|
||||||
|
],
|
||||||
|
text: "this is a long test.",
|
||||||
|
evaluation: "25.23",
|
||||||
|
});
|
||||||
|
|
||||||
|
article.authors = article.authors.map((author) => {
|
||||||
|
author.name = "TEST";
|
||||||
|
return author;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(article.save()).toStrictEqual({
|
||||||
|
id: 1,
|
||||||
|
authors: [
|
||||||
|
{ name: "TEST", },
|
||||||
|
{}, //{ name: "TEST", firstName: "Another", email: "another@test.test" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"files": ["src/index.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./lib/",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"module": "ES6",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES5",
|
||||||
|
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue