Models rewrite with new API for better typings and extensibility.

This commit is contained in:
Madeorsk 2024-10-03 23:33:00 +02:00
parent 498d25a909
commit e43e27e2e1
Signed by: Madeorsk
GPG Key ID: 677E51CA765BB79F
17 changed files with 430 additions and 386 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@sharkitek/core", "name": "@sharkitek/core",
"version": "2.1.3", "version": "3.0.0",
"description": "Sharkitek core models library.", "description": "Sharkitek core models library.",
"keywords": [ "keywords": [
"sharkitek", "sharkitek",

View File

@ -1,34 +0,0 @@
import {Type} from "./Types/Type";
/**
* Options of a definition.
*/
export interface DefinitionOptions<SerializedType, SharkitekType>
{ //TODO implement some options, like `mandatory`.
}
/**
* A Sharkitek model property definition.
*/
export class Definition<SerializedType, SharkitekType>
{
/**
* Initialize a property definition with the given type and options.
* @param type - The model property type.
* @param options - Property definition options.
*/
constructor(
public type: Type<SerializedType, SharkitekType>,
public options: DefinitionOptions<SerializedType, SharkitekType> = {},
) {}
}
/**
* Initialize a property definition with the given type and options.
* @param type - The model property type.
* @param options - Property definition options.
*/
export function SDefine<SerializedType, SharkitekType>(type: Type<SerializedType, SharkitekType>, options: DefinitionOptions<SerializedType, SharkitekType> = {}): Definition<SerializedType, SharkitekType>
{
return new Definition<SerializedType, SharkitekType>(type, options);
}

View File

@ -1,85 +1,123 @@
import {Definition} from "./Definition"; import {Definition} from "./PropertyDefinition";
/** /**
* Model properties definition type. * Type definition of a model constructor.
*/ */
export type ModelDefinition<T> = Partial<Record<keyof T, Definition<unknown, unknown>>>; export type ConstructorOf<T extends object> = { new(): T; };
/**
* Model identifier type.
*/
export type ModelIdentifier<T> = keyof T;
/** /**
* A Sharkitek model. * Unknown property definition.
*/ */
export abstract class Model<THIS> 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>;
/**
* Identifier type.
*/
export type IdentifierType<Shape extends ModelShape, K extends keyof Shape> = Shape[K]["_sharkitek"];
/**
* Interface of a Sharkitek model definition.
*/
export interface ModelDefinition<Shape extends ModelShape, IdentifierType>
{ {
/** /**
* Model properties definition function. * Get model identifier.
*/ */
protected abstract SDefinition(): ModelDefinition<THIS>; getIdentifier(): IdentifierType;
/** /**
* Return the name of the model identifier property. * Serialize the model.
*/ */
protected SIdentifier(): ModelIdentifier<THIS> serialize(): SerializedModel<Shape>;
{ /**
return undefined; * Deserialize the model.
* @param obj Serialized object.
*/
deserialize(obj: SerializedModel<Shape>): Model<Shape, IdentifierType>;
/**
* 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()`.
*/
save(): Partial<SerializedModel<Shape>>;
} }
/** /**
* Get given property definition. * Define a Sharkitek model.
* @protected * @param shape Model shape definition.
* @param identifier Identifier property name.
*/ */
protected getPropertyDefinition(propertyName: string): Definition<unknown, unknown> export function model<Shape extends ModelShape, Identifier extends keyof Shape = any>(
shape: Shape,
identifier?: Identifier,
): ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>
{ {
return (this.SDefinition() as any)?.[propertyName]; // Get shape entries.
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
return class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>>
{
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>
);
} }
/**
* Get the list of the model properties.
* @protected
*/
protected getProperties(): string[]
{
return Object.keys(this.SDefinition());
}
/**
* Calling a function for a defined property.
* @param propertyName - The property for which to check definition.
* @param callback - The function called when the property is defined.
* @param notProperty - The function called when the property is not defined.
* @protected
*/
protected propertyWithDefinition(propertyName: string, callback: (propertyDefinition: Definition<unknown, unknown>) => void, notProperty: () => void = () => {}): unknown
{
// Getting the current property definition.
const propertyDefinition = this.getPropertyDefinition(propertyName);
if (propertyDefinition)
// There is a definition for the current property, calling the right callback.
return callback(propertyDefinition);
else
// No definition for the given property, calling the right callback.
return notProperty();
}
/** /**
* Calling a function for each defined property. * Calling a function for each defined property.
* @param callback - The function to call. * @param callback - The function to call.
* @protected * @protected
*/ */
protected forEachModelProperty(callback: (propertyName: string, propertyDefinition: Definition<unknown, unknown>) => unknown): any|void protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
{ {
for (const propertyName of this.getProperties()) for (const [propertyName, propertyDefinition] of shapeEntries)
{ // For each property, checking that its type is defined and calling the callback with its type. { // For each property, checking that its type is defined and calling the callback with its type.
const result = this.propertyWithDefinition(propertyName, (propertyDefinition) => {
// If the property is defined, calling the function with the property name and definition. // If the property is defined, calling the function with the property name and definition.
const result = callback(propertyName, propertyDefinition); const result = callback(propertyName, propertyDefinition);
// If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result;
});
// If there is a return value, returning it directly (loop is broken). // If there is a return value, returning it directly (loop is broken).
if (typeof result !== "undefined") return result; if (typeof result !== "undefined") return result;
} }
@ -90,30 +128,60 @@ export abstract class Model<THIS>
* The original properties values. * The original properties values.
* @protected * @protected
*/ */
protected _originalProperties: Record<string, any> = {}; protected _originalProperties: Partial<PropertiesModel<Shape>> = {};
/** /**
* The original (serialized) object. * The original (serialized) object.
* @protected * @protected
*/ */
protected _originalObject: any = null; protected _originalObject: SerializedModel<Shape>|null = null;
getIdentifier(): IdentifierType<Shape, Identifier>
{
return (this as PropertiesModel<Shape>)?.[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>): Model<Shape, IdentifierType<Shape, Identifier>>
{
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 Model<Shape, IdentifierType<Shape, Identifier>>;
}
/**
* Determine if the model is new or not.
*/
isNew(): boolean isNew(): boolean
{ {
return !this._originalObject; return !this._originalObject;
} }
/**
* Determine if the model is dirty or not.
*/
isDirty(): boolean isDirty(): boolean
{ {
return this.forEachModelProperty((propertyName, propertyDefinition) => ( return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different. // For each property, checking if it is different.
propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]) propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
// There is a difference, we should return false. // There is a difference, we should return false.
? true ? true
// There is no difference, returning nothing. // There is no difference, returning nothing.
@ -121,97 +189,43 @@ export abstract class Model<THIS>
)) === true; )) === true;
} }
/**
* Get model identifier.
*/
getIdentifier(): unknown
{
return (this as any)[this.SIdentifier()];
}
/** serializeDiff(): Partial<SerializedModel<Shape>>
* Set current properties values as original values.
*/
resetDiff()
{ {
this.forEachModelProperty((propertyName, propertyDefinition) => { // Creating an empty (=> partial) serialized object.
// For each property, set its original value to its current property value. const serializedObject: Partial<SerializedModel<Shape>> = {};
this._originalProperties[propertyName] = (this as any)[propertyName];
propertyDefinition.type.resetDiff((this as any)[propertyName]);
});
}
/**
* Serialize the difference between current model state and original one.
*/
serializeDiff(): any
{
// Creating a serialized object.
const serializedDiff: any = {};
this.forEachModelProperty((propertyName, propertyDefinition) => { this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object if it has changed. // For each defined model property, adding it to the serialized object if it has changed or if it is the identifier.
if (this.SIdentifier() == propertyName if (
|| propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])) identifier == propertyName ||
// Adding the current property to the serialized object if it is the identifier or its value has changed. propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as PropertiesModel<Shape>)[propertyName])
serializedDiff[propertyName] = propertyDefinition.type.serializeDiff((this as any)[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 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(): any
{
// Creating a serialized object.
const serializedObject: any = {};
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object.
serializedObject[propertyName] = propertyDefinition.type.serialize((this as any)[propertyName]);
}); });
return serializedObject; // Returning the serialized object. return serializedObject; // Returning the serialized object.
} }
/** resetDiff(): void
* Special operations on parse.
* @protected
*/
protected parse(): void
{} // Nothing by default. TODO: create an event system to create functions like "beforeDeserialization" or "afterDeserialization".
/**
* Deserialize the model.
*/
deserialize(serializedObject: any): THIS
{ {
this.forEachModelProperty((propertyName, propertyDefinition) => { this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, assigning its deserialized value to the model. // For each property, set its original value to its current property value.
(this as any)[propertyName] = propertyDefinition.type.deserialize(serializedObject[propertyName]); this._originalProperties[propertyName] = (this as PropertiesModel<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
}); });
}
// Reset original property values. save(): Partial<SerializedModel<Shape>>
{
// Get the difference.
const diff = this.serializeDiff();
// Once the difference has been obtained, reset it.
this.resetDiff(); this.resetDiff();
this._originalObject = serializedObject; // The model is not a new one, but loaded from a deserialized one. return diff; // Return the difference.
}
return this as unknown as THIS; // Returning this, after deserialization. } as unknown as ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>;
}
} }

10
src/Model/Properties.ts Normal file
View File

@ -0,0 +1,10 @@
export {define} from "./PropertyDefinition";
export {array} from "./Types/ArrayType";
export {bool, boolean} from "./Types/BoolType";
export {date} from "./Types/DateType";
export {decimal} from "./Types/DecimalType";
export {model} from "./Types/ModelType";
export {numeric} from "./Types/NumericType";
export {object} from "./Types/ObjectType";
export {string} from "./Types/StringType";

View File

@ -0,0 +1,26 @@
import {Type} from "./Types/Type";
/**
* Property definition class.
*/
export class Definition<SerializedType, ModelType>
{
readonly _sharkitek: ModelType;
readonly _serialized: SerializedType;
/**
* Create a property definer instance.
* @param type Property type.
*/
constructor(public readonly type: Type<SerializedType, ModelType>)
{}
}
/**
* New definition of a property of the given type.
* @param type Type of the property to define.
*/
export function define<SerializedType, ModelType>(type: Type<SerializedType, ModelType>): Definition<SerializedType, ModelType>
{
return new Definition(type);
}

View File

@ -1,4 +1,5 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of an array of values. * Type of an array of values.
@ -6,60 +7,60 @@ import {Type} from "./Type";
export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<SerializedValueType[], SharkitekValueType[]> export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<SerializedValueType[], SharkitekValueType[]>
{ {
/** /**
* Constructs a new array type of a Sharkitek model property. * Initialize a new array type of a Sharkitek model property.
* @param valueType - Type of the array values. * @param valueDefinition Definition the array values.
*/ */
constructor(protected valueType: Type<SerializedValueType, SharkitekValueType>) constructor(protected valueDefinition: Definition<SerializedValueType, SharkitekValueType>)
{ {
super(); super();
} }
serialize(value: SharkitekValueType[]): SerializedValueType[] serialize(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return value.map((value) => ( return value.map((value) => (
// Serializing each value of the array. // Serializing each value of the array.
this.valueType.serialize(value) this.valueDefinition.type.serialize(value)
)); ));
} }
deserialize(value: SerializedValueType[]): SharkitekValueType[] deserialize(value: SerializedValueType[]|null|undefined): SharkitekValueType[]|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return value.map((serializedValue) => ( return value.map((serializedValue) => (
// Deserializing each value of the array. // Deserializing each value of the array.
this.valueType.deserialize(serializedValue) this.valueDefinition.type.deserialize(serializedValue)
)); ));
} }
serializeDiff(value: SharkitekValueType[]): any serializeDiff(value: SharkitekValueType[]|null|undefined): SerializedValueType[]|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
// Serializing diff of all elements. // Serializing diff of all elements.
return value.map((value) => this.valueType.serializeDiff(value)); return value.map((value) => this.valueDefinition.type.serializeDiff(value) as SerializedValueType);
} }
resetDiff(value: SharkitekValueType[]): void resetDiff(value: SharkitekValueType[]|null|undefined): void
{ {
// Do nothing if it is not an array. // Do nothing if it is not an array.
if (!Array.isArray(value)) return; if (!Array.isArray(value)) return;
// Reset diff of all elements. // Reset diff of all elements.
value.forEach((value) => this.valueType.resetDiff(value)); value.forEach((value) => this.valueDefinition.type.resetDiff(value));
} }
} }
/** /**
* Type of an array of values. * New array property definition.
* @param valueType - Type of the array values. * @param valueDefinition Array values type definition.
*/ */
export function SArray<SerializedValueType, SharkitekValueType>(valueType: Type<SerializedValueType, SharkitekValueType>) export function array<SerializedValueType, SharkitekValueType>(valueDefinition: Definition<SerializedValueType, SharkitekValueType>): Definition<SerializedValueType[], SharkitekValueType[]>
{ {
return new ArrayType<SerializedValueType, SharkitekValueType>(valueType); return define(new ArrayType(valueDefinition));
} }

View File

@ -1,22 +1,42 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of any boolean value. * Type of any boolean value.
*/ */
export class BoolType extends Type<boolean, boolean> export class BoolType extends Type<boolean, boolean>
{ {
deserialize(value: boolean): boolean deserialize(value: boolean|null|undefined): boolean|null|undefined
{ {
// Keep NULL and undefined values.
if (value === undefined) return undefined;
if (value === null) return null;
return !!value; // ensure bool type. return !!value; // ensure bool type.
} }
serialize(value: boolean): boolean serialize(value: boolean|null|undefined): boolean|null|undefined
{ {
// Keep NULL and undefined values.
if (value === undefined) return undefined;
if (value === null) return null;
return !!value; // ensure bool type. return !!value; // ensure bool type.
} }
} }
/** /**
* Type of any boolean value. * New boolean property definition.
*/ */
export const SBool = new BoolType(); export function bool(): Definition<boolean, boolean>
{
return define(new BoolType());
}
/**
* New boolean property definition.
* Alias of bool.
*/
export function boolean(): ReturnType<typeof bool>
{
return bool();
}

View File

@ -1,22 +1,32 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of dates. * Type of dates.
*/ */
export class DateType extends Type<string, Date> export class DateType extends Type<string, Date>
{ {
deserialize(value: string): Date deserialize(value: string|null|undefined): Date|null|undefined
{ {
if (value === undefined) return undefined;
if (value === null) return null;
return new Date(value); return new Date(value);
} }
serialize(value: Date): string serialize(value: Date|null|undefined): string|null|undefined
{ {
if (value === undefined) return undefined;
if (value === null) return null;
return value?.toISOString(); return value?.toISOString();
} }
} }
/** /**
* Type of dates. * New date property definition.
*/ */
export const SDate = new DateType(); export function date(): Definition<string, Date>
{
return define(new DateType());
}

View File

@ -1,22 +1,32 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of decimal numbers. * Type of decimal numbers.
*/ */
export class DecimalType extends Type<string, number> export class DecimalType extends Type<string, number>
{ {
deserialize(value: string): number deserialize(value: string|null|undefined): number|null|undefined
{ {
if (value === undefined) return undefined;
if (value === null) return null;
return parseFloat(value); return parseFloat(value);
} }
serialize(value: number): string serialize(value: number|null|undefined): string|null|undefined
{ {
if (value === undefined) return undefined;
if (value === null) return null;
return value?.toString(); return value?.toString();
} }
} }
/** /**
* Type of decimal numbers. * New decimal property definition.
*/ */
export const SDecimal = new DecimalType(); export function decimal(): Definition<string, number>
{
return define(new DecimalType());
}

View File

@ -1,44 +1,49 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {Model} from "../Model"; import {define, Definition} from "../PropertyDefinition";
import {ConstructorOf, Model, ModelShape, SerializedModel} from "../Model";
/**
* Type definition of the constructor of a specific type.
*/
export type ConstructorOf<T> = { new(): T; }
/** /**
* Type of a Sharkitek model value. * Type of a Sharkitek model value.
*/ */
export class ModelType<M extends Model<M>> extends Type<any, M> export class ModelType<Shape extends ModelShape> extends Type<SerializedModel<Shape>, Model<Shape>>
{ {
/** /**
* Constructs a new model type of a Sharkitek model property. * Initialize a new model type of a Sharkitek model property.
* @param modelConstructor - Constructor of the model. * @param modelConstructor Model constructor.
*/ */
constructor(protected modelConstructor: ConstructorOf<M>) constructor(protected modelConstructor: ConstructorOf<Model<Shape>>)
{ {
super(); super();
} }
serialize(value: M|null): any serialize(value: Model<Shape>|null|undefined): SerializedModel<Shape>|null|undefined
{ {
if (value === undefined) return undefined;
if (value === null) return null;
// Serializing the given model. // Serializing the given model.
return value ? value.serialize() : null; return value?.serialize();
} }
deserialize(value: any): M|null 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. // Deserializing the given object in the new model.
return value ? (new this.modelConstructor()).deserialize(value) : null; return (new this.modelConstructor()).deserialize(value) as Model<Shape>;
} }
serializeDiff(value: M): any 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. // Serializing the given model.
return value ? value.serializeDiff() : null; return value?.serializeDiff();
} }
resetDiff(value: M): void resetDiff(value: Model<Shape>|null|undefined): void
{ {
// Reset diff of the given model. // Reset diff of the given model.
value?.resetDiff(); value?.resetDiff();
@ -46,10 +51,10 @@ export class ModelType<M extends Model<M>> extends Type<any, M>
} }
/** /**
* Type of a Sharkitek model value. * New model property definition.
* @param modelConstructor - Constructor of the model. * @param modelConstructor Model constructor.
*/ */
export function SModel<M extends Model<M>>(modelConstructor: ConstructorOf<M>) export function model<Shape extends ModelShape>(modelConstructor: ConstructorOf<Model<Shape>>): Definition<SerializedModel<Shape>, Model<Shape>>
{ {
return new ModelType(modelConstructor); return define(new ModelType(modelConstructor));
} }

View File

@ -1,22 +1,26 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of any numeric value. * Type of any numeric value.
*/ */
export class NumericType extends Type<number, number> export class NumericType extends Type<number, number>
{ {
deserialize(value: number): number deserialize(value: number|null|undefined): number|null|undefined
{ {
return value; return value;
} }
serialize(value: number): number serialize(value: number|null|undefined): number|null|undefined
{ {
return value; return value;
} }
} }
/** /**
* Type of any numeric value. * New numeric property definition.
*/ */
export const SNumeric = new NumericType(); export function numeric(): Definition<number, number>
{
return define(new NumericType());
}

View File

@ -1,66 +1,67 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {Definition} from "../Definition"; import {define, Definition} from "../PropertyDefinition";
import {ModelShape, PropertiesModel, SerializedModel, UnknownDefinition} from "../Model";
/** /**
* Type of a simple object. * Type of a custom object.
*/ */
export class ObjectType<Keys extends symbol|string> extends Type<Record<Keys, any>, Record<Keys, any>> export class ObjectType<Shape extends ModelShape> extends Type<SerializedModel<Shape>, PropertiesModel<Shape>>
{ {
/** /**
* Constructs a new object type of a Sharkitek model property. * Initialize a new object type of a Sharkitek model property.
* @param fieldsTypes Object fields types. * @param shape
*/ */
constructor(protected fieldsTypes: Record<Keys, Definition<unknown, unknown>>) constructor(protected readonly shape: Shape)
{ {
super(); super();
} }
deserialize(value: Record<Keys, any>): Record<Keys, any> deserialize(value: SerializedModel<Shape>|null|undefined): PropertiesModel<Shape>|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return Object.fromEntries( return Object.fromEntries(
// For each defined field, deserialize its value according to its type. // For each defined field, deserialize its value according to its type.
(Object.entries(this.fieldsTypes) as [Keys, Definition<any, any>][]).map(([fieldName, fieldDefinition]) => ( (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the deserialized value. // Return an entry with the current field name and the deserialized value.
[fieldName, fieldDefinition.type.deserialize(value?.[fieldName])] [fieldName, fieldDefinition.type.deserialize(value?.[fieldName])]
)) ))
) as Record<Keys, any>; ) as PropertiesModel<Shape>;
} }
serialize(value: Record<Keys, any>): Record<Keys, any> serialize(value: PropertiesModel<Shape>|null|undefined): SerializedModel<Shape>|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return Object.fromEntries( return Object.fromEntries(
// For each defined field, serialize its value according to its type. // For each defined field, serialize its value according to its type.
(Object.entries(this.fieldsTypes) as [Keys, Definition<any, any>][]).map(([fieldName, fieldDefinition]) => ( (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the serialized value. // Return an entry with the current field name and the serialized value.
[fieldName, fieldDefinition.type.serialize(value?.[fieldName])] [fieldName, fieldDefinition.type.serialize(value?.[fieldName])]
)) ))
) as Record<Keys, any>; ) as PropertiesModel<Shape>;
} }
serializeDiff(value: Record<Keys, any>): Record<Keys, any> serializeDiff(value: PropertiesModel<Shape>|null|undefined): Partial<SerializedModel<Shape>>|null|undefined
{ {
if (value === undefined) return undefined; if (value === undefined) return undefined;
if (value === null) return null; if (value === null) return null;
return Object.fromEntries( return Object.fromEntries(
// For each defined field, serialize its diff value according to its type. // For each defined field, serialize its diff value according to its type.
(Object.entries(this.fieldsTypes) as [Keys, Definition<any, any>][]).map(([fieldName, fieldDefinition]) => ( (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).map(([fieldName, fieldDefinition]) => (
// Return an entry with the current field name and the serialized diff value. // Return an entry with the current field name and the serialized diff value.
[fieldName, fieldDefinition.type.serializeDiff(value?.[fieldName])] [fieldName, fieldDefinition.type.serializeDiff(value?.[fieldName])]
)) ))
) as Record<Keys, any>; ) as PropertiesModel<Shape>;
} }
resetDiff(value: Record<Keys, any>): void resetDiff(value: PropertiesModel<Shape>|null|undefined)
{ {
// For each field, reset its diff. // For each field, reset its diff.
(Object.entries(this.fieldsTypes) as [Keys, Definition<any, any>][]).forEach(([fieldName, fieldDefinition]) => { (Object.entries(this.shape) as [keyof Shape, UnknownDefinition][]).forEach(([fieldName, fieldDefinition]) => {
// Reset diff of the current field. // Reset diff of the current field.
fieldDefinition.type.resetDiff(value?.[fieldName]); fieldDefinition.type.resetDiff(value?.[fieldName]);
}); });
@ -68,10 +69,10 @@ export class ObjectType<Keys extends symbol|string> extends Type<Record<Keys, an
} }
/** /**
* Type of a simple object. * New object property definition.
* @param fieldsTypes Object fields types. * @param shape Shape of the object.
*/ */
export function SObject<Keys extends symbol|string>(fieldsTypes: Record<Keys, Definition<unknown, unknown>>): ObjectType<Keys> export function object<Shape extends ModelShape>(shape: Shape): Definition<SerializedModel<Shape>, PropertiesModel<Shape>>
{ {
return new ObjectType<Keys>(fieldsTypes); return define(new ObjectType(shape));
} }

View File

@ -1,22 +1,26 @@
import {Type} from "./Type"; import {Type} from "./Type";
import {define, Definition} from "../PropertyDefinition";
/** /**
* Type of any string value. * Type of any string value.
*/ */
export class StringType extends Type<string, string> export class StringType extends Type<string, string>
{ {
deserialize(value: string): string deserialize(value: string|null|undefined): string|null|undefined
{ {
return value; return value;
} }
serialize(value: string): string serialize(value: string|null|undefined): string|null|undefined
{ {
return value; return value;
} }
} }
/** /**
* Type of any string value. * New string property definition.
*/ */
export const SString = new StringType(); export function string(): Definition<string, string>
{
return define(new StringType());
}

View File

@ -1,25 +1,25 @@
/** /**
* Abstract class of a Sharkitek model property type. * Abstract class of a Sharkitek model property type.
*/ */
export abstract class Type<SerializedType, SharkitekType> export abstract class Type<SerializedType, ModelType>
{ {
/** /**
* Serialize the given value of a Sharkitek model property. * Serialize the given value of a Sharkitek model property.
* @param value - Value to serialize. * @param value Value to serialize.
*/ */
abstract serialize(value: SharkitekType): SerializedType; abstract serialize(value: ModelType|null|undefined): SerializedType|null|undefined;
/** /**
* Deserialize the given value of a serialized Sharkitek model. * Deserialize the given value of a serialized Sharkitek model.
* @param value - Value to deserialize. * @param value - Value to deserialize.
*/ */
abstract deserialize(value: SerializedType): SharkitekType; abstract deserialize(value: SerializedType|null|undefined): ModelType|null|undefined;
/** /**
* Serialize the given value only if it has changed. * Serialize the given value only if it has changed.
* @param value - Value to deserialize. * @param value - Value to deserialize.
*/ */
serializeDiff(value: SharkitekType): SerializedType|null serializeDiff(value: ModelType|null|undefined): Partial<SerializedType>|null|undefined
{ {
return this.serialize(value); // By default, nothing changes. return this.serialize(value); // By default, nothing changes.
} }
@ -28,7 +28,7 @@ export abstract class Type<SerializedType, SharkitekType>
* Reset the difference between the original value and the current one. * 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: SharkitekType): void resetDiff(value: ModelType|null|undefined): void
{ {
// By default, nothing to do. // By default, nothing to do.
} }
@ -38,7 +38,7 @@ export abstract class Type<SerializedType, SharkitekType>
* @param originalValue - Original property value. * @param originalValue - Original property value.
* @param currentValue - Current property value. * @param currentValue - Current property value.
*/ */
propertyHasChanged(originalValue: SharkitekType, currentValue: SharkitekType): boolean propertyHasChanged(originalValue: ModelType|null|undefined, currentValue: ModelType|null|undefined): boolean
{ {
return originalValue != currentValue; return originalValue != currentValue;
} }
@ -48,7 +48,7 @@ export abstract class Type<SerializedType, SharkitekType>
* @param originalValue - Original serialized property value. * @param originalValue - Original serialized property value.
* @param currentValue - Current serialized property value. * @param currentValue - Current serialized property value.
*/ */
serializedPropertyHasChanged(originalValue: SerializedType, currentValue: SerializedType): boolean serializedPropertyHasChanged(originalValue: SerializedType|null|undefined, currentValue: SerializedType|null|undefined): boolean
{ {
return originalValue != currentValue; return originalValue != currentValue;
} }

14
src/Model/index.ts Normal file
View File

@ -0,0 +1,14 @@
import * as property from "./Properties";
export { property };
export * from "./Model";
export {Definition} from "./PropertyDefinition";
export {ArrayType} from "./Types/ArrayType";
export {BoolType} from "./Types/BoolType";
export {DateType} from "./Types/DateType";
export {DecimalType} from "./Types/DecimalType";
export {ModelType} from "./Types/ModelType";
export {NumericType} from "./Types/NumericType";
export {ObjectType} from "./Types/ObjectType";
export {StringType} from "./Types/StringType";

View File

@ -1,14 +1,4 @@
import * as s from "./Model";
export * from "./Model/Model"; export * from "./Model";
export { s };
export * from "./Model/Definition"; export default s;
export * from "./Model/Types/Type";
export * from "./Model/Types/ArrayType";
export * from "./Model/Types/BoolType";
export * from "./Model/Types/DateType";
export * from "./Model/Types/DecimalType";
export * from "./Model/Types/ModelType";
export * from "./Model/Types/NumericType";
export * from "./Model/Types/ObjectType";
export * from "./Model/Types/StringType";

View File

@ -1,39 +1,18 @@
import { import {s} from "../src";
SArray,
SDecimal,
SModel,
SNumeric,
SString,
SDate,
SBool,
Model,
ModelDefinition,
SDefine, ModelIdentifier
} from "../src";
import {SObject} from "../src/Model/Types/ObjectType";
/** /**
* Another test model. * Another test model.
*/ */
class Author extends Model<Author> class Author extends s.model({
name: s.property.string(),
firstName: s.property.string(),
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.bool(),
})
{ {
name: string;
firstName: string;
email: string;
createdAt: Date;
active: boolean = true; active: boolean = true;
protected SDefinition(): ModelDefinition<Author>
{
return {
name: SDefine(SString),
firstName: SDefine(SString),
email: SDefine(SString),
createdAt: SDefine(SDate),
active: SDefine(SBool),
};
}
constructor(name: string = "", firstName: string = "", email: string = "", createdAt: Date = new Date()) constructor(name: string = "", firstName: string = "", email: string = "", createdAt: Date = new Date())
{ {
super(); super();
@ -48,7 +27,18 @@ class Author extends Model<Author>
/** /**
* A test model. * A test model.
*/ */
class Article extends Model<Article> 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; id: number;
title: string; title: string;
@ -58,27 +48,6 @@ class Article extends Model<Article>
tags: { tags: {
name: string; name: string;
}[]; }[];
protected SIdentifier(): ModelIdentifier<Article>
{
return "id";
}
protected SDefinition(): ModelDefinition<Article>
{
return {
id: SDefine(SNumeric),
title: SDefine(SString),
authors: SDefine(SArray(SModel(Author))),
text: SDefine(SString),
evaluation: SDefine(SDecimal),
tags: SDefine(SArray(
SObject({
name: SDefine(SString),
})
)),
};
}
} }
it("deserialize", () => { it("deserialize", () => {
@ -140,8 +109,8 @@ it("deserialize then save", () => {
id: 1, id: 1,
title: "this is a test", title: "this is a test",
authors: [ authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, }, { 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(), active: false, }, { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, },
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: "25.23", evaluation: "25.23",
@ -167,8 +136,8 @@ it("save with modified submodels", () => {
id: 1, id: 1,
title: "this is a test", title: "this is a test",
authors: [ authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: new Date(), active: true, }, { 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(), active: false, }, { name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).toISOString(), active: false, },
], ],
text: "this is a long test.", text: "this is a long test.",
evaluation: "25.23", evaluation: "25.23",