Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
6eee1b709e | |||
8f8dafed5b | |||
ff9cb91f73 | |||
e373efdd0a | |||
4eb8b7d3bc | |||
576338fa62 | |||
62e62f962e | |||
22bc42acba | |||
6af0da6b55 | |||
6a14623355 | |||
3e291d6bd5 |
11 changed files with 385 additions and 160 deletions
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) <year> <copyright holders>
|
Copyright (c) 2024 Zeptotech
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|
30
README.md
30
README.md
|
@ -1,13 +1,33 @@
|
||||||
# Sharkitek Core
|
<p align="center">
|
||||||
|
<a href="https://code.zeptotech.net/Sharkitek/Core">
|
||||||
|
<picture>
|
||||||
|
<img alt="Sharkitek logo" width="200" src="https://code.zeptotech.net/Sharkitek/Core/raw/branch/main/logo.svg" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<h1 align="center">
|
||||||
|
Sharkitek
|
||||||
|
</h1>
|
||||||
|
|
||||||
![Version 3.0.0](https://img.shields.io/badge/version-3.0.0-blue)
|
<h4 align="center">
|
||||||
|
<a href="https://code.zeptotech.net/Sharkitek/Core">Documentation</a> |
|
||||||
|
<a href="https://code.zeptotech.net/Sharkitek/Core">Website</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
TypeScript library for well-designed model architectures
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img alt="Version 3.3.0" src="https://img.shields.io/badge/version-3.3.0-blue" />
|
||||||
|
</p>
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Sharkitek is a Javascript / TypeScript library designed to ease development of client-side models.
|
Sharkitek is a Javascript / TypeScript library designed to ease development of client-side models.
|
||||||
|
|
||||||
With Sharkitek, you define the architecture of your models by specifying their properties and their types.
|
With Sharkitek, you define the architecture of your models by specifying their properties and their types.
|
||||||
Then, you can use the defined methods like `serialize`, `deserialize`, `save` or `serializeDiff`.
|
Then, you can use the defined methods like `serialize`, `deserialize`, `patch` or `serializeDiff`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class Example extends s.model({
|
class Example extends s.model({
|
||||||
|
@ -207,7 +227,7 @@ const result = model.serializeDiff();
|
||||||
// result = {}
|
// result = {}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `save()`
|
#### `patch()`
|
||||||
|
|
||||||
Get difference between original values and current ones, then reset it.
|
Get difference between original values and current ones, then reset it.
|
||||||
Similar to call `serializeDiff()` then `resetDiff()`.
|
Similar to call `serializeDiff()` then `resetDiff()`.
|
||||||
|
@ -226,7 +246,7 @@ const model = (new TestModel()).deserialize({
|
||||||
|
|
||||||
model.title = "A new title for a new world";
|
model.title = "A new title for a new world";
|
||||||
|
|
||||||
const result = model.save();
|
const result = model.patch();
|
||||||
// if `id` is defined as the model identifier:
|
// if `id` is defined as the model identifier:
|
||||||
// result = { id: 5, title: "A new title for a new world" }
|
// result = { id: 5, title: "A new title for a new world" }
|
||||||
// if `id` is not defined as the model identifier:
|
// if `id` is not defined as the model identifier:
|
||||||
|
|
37
logo.svg
Normal file
37
logo.svg
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
fill="#000000"
|
||||||
|
width="900"
|
||||||
|
height="900"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<title
|
||||||
|
id="title1">shark</title>
|
||||||
|
<path
|
||||||
|
d="M 18.033615,1.4513338 C 10.403464,6.2983278 6.2910007,18.59117 6.2910007,34.205527 H 29.708999 c 0,-15.613237 -4.045235,-27.9071992 -11.675384,-32.7541932 z m -7.467688,15.9532972 1.284018,1.073376 -0.440332,-2.462712 c 0.5154,-0.767498 1.064412,-1.447601 1.641435,-2.029105 l 1.345642,1.913699 0.275626,-3.236931 c 0.657695,-0.416801 1.342279,-0.713715 2.045911,-0.877298 l 1.281776,2.534419 1.281775,-2.534419 c 0.69915,0.162462 1.378133,0.456016 2.032467,0.869455 l 0.28907,3.393792 1.406144,-2.001093 c 0.554614,0.568059 1.082339,1.226874 1.579811,1.96636 l -0.43921,2.462712 1.284016,-1.073375 c 1.706421,3.099118 2.943378,7.246962 3.466621,11.948299 -0.06683,3.889694 -21.7515603,2.320881 -21.8036319,-0.0011 0.5221219,-4.700217 1.7590797,-8.849181 3.4666199,-11.947179 z"
|
||||||
|
id="path2"
|
||||||
|
style="display:inline;fill:#1c4878;fill-opacity:1;stroke-width:1.12043" />
|
||||||
|
<path
|
||||||
|
d="M 18.030001,4.0195273 C 11.220001,8.3455277 6.2910007,20.269527 6.2910007,34.205527 H 29.708999 c 0,-13.935 -4.869,-25.8599993 -11.678998,-30.1859997 z m -6.664999,15.1909997 1.146,0.958 -0.393,-2.198 c 0.46,-0.685 0.949999,-1.292 1.465,-1.811 l 1.200997,1.708 0.246,-2.889 c 0.587,-0.372 1.198,-0.637 1.826,-0.783 l 1.144,2.262 1.144,-2.262 c 0.624,0.145 1.23,0.407 1.814,0.776 l 0.258,3.029 1.255,-1.786 c 0.495,0.507 0.966,1.095 1.41,1.755 l -0.392,2.198 1.146,-0.958 c 1.523,2.766 2.627,6.468 3.094,10.664 -0.626,-2.009 -1.659,-3.774 -2.975,-5.146 l 0.25,-2.235 -1.6,1.042 c -0.381,-0.283 -0.777,-0.537 -1.188,-0.759 l -0.208,-2.556 -1.641,1.801 c -0.456,-0.13 -0.924,-0.222 -1.401,-0.276 l -0.968,-2.074 -0.968,2.074 c -0.508,0.057 -1.006,0.159 -1.49,0.302 l -1.549999,-1.701 -0.197,2.425 c -0.415,0.224 -0.815,0.479 -1.199,0.765 l -1.6,-1.042 0.25,2.234 c -1.3159993,1.371 -2.3489993,3.136 -2.9749993,5.145 0.466,-4.195 1.57,-7.898 3.0939993,-10.663 z"
|
||||||
|
id="path1"
|
||||||
|
style="display:inline;fill:#3178c6;fill-opacity:1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>shark</dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
23
package.json
23
package.json
|
@ -1,18 +1,23 @@
|
||||||
{
|
{
|
||||||
"name": "@sharkitek/core",
|
"name": "@sharkitek/core",
|
||||||
"version": "3.0.0",
|
"version": "3.3.0",
|
||||||
"description": "Sharkitek core models library.",
|
"description": "TypeScript library for well-designed model architectures.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"sharkitek",
|
"deserialization",
|
||||||
"model",
|
|
||||||
"serialization",
|
|
||||||
"diff",
|
"diff",
|
||||||
"dirty",
|
"dirty",
|
||||||
"deserialization",
|
"model",
|
||||||
"property"
|
"object",
|
||||||
|
"property",
|
||||||
|
"serialization",
|
||||||
|
"sharkitek",
|
||||||
|
"typescript"
|
||||||
],
|
],
|
||||||
"repository": "https://git.madeorsk.com/Sharkitek/core",
|
"repository": "https://code.zeptotech.net/Sharkitek/Core",
|
||||||
"author": "Madeorsk <madeorsk@protonmail.com>",
|
"author": {
|
||||||
|
"name": "Madeorsk",
|
||||||
|
"email": "madeorsk@protonmail.com"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|
|
@ -34,21 +34,46 @@ export type SerializedModel<Shape extends ModelShape> = {
|
||||||
*/
|
*/
|
||||||
export type Model<Shape extends ModelShape, IdentifierType = unknown> = ModelDefinition<Shape, IdentifierType> & PropertiesModel<Shape>;
|
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.
|
* Identifier type.
|
||||||
*/
|
*/
|
||||||
export type IdentifierType<Shape extends ModelShape, K extends keyof Shape> = Shape[K]["_sharkitek"];
|
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.
|
* Interface of a Sharkitek model definition.
|
||||||
*/
|
*/
|
||||||
export interface ModelDefinition<Shape extends ModelShape, IdentifierType>
|
export interface ModelDefinition<Shape extends ModelShape, IdentifierType, ModelType extends Model<Shape, IdentifierType> = Model<Shape, IdentifierType>>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get model identifier.
|
* Get model identifier.
|
||||||
*/
|
*/
|
||||||
getIdentifier(): IdentifierType;
|
getIdentifier(): IdentifierType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model identifier name.
|
||||||
|
*/
|
||||||
|
getIdentifierName(): IdentifierNameType<Shape>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize the model.
|
* Serialize the model.
|
||||||
*/
|
*/
|
||||||
|
@ -57,7 +82,7 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType>
|
||||||
* Deserialize the model.
|
* Deserialize the model.
|
||||||
* @param obj Serialized object.
|
* @param obj Serialized object.
|
||||||
*/
|
*/
|
||||||
deserialize(obj: SerializedModel<Shape>): Model<Shape, IdentifierType>;
|
deserialize(obj: SerializedModel<Shape>): ModelType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find out if the model is new (never deserialized) or not.
|
* Find out if the model is new (never deserialized) or not.
|
||||||
|
@ -80,7 +105,7 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType>
|
||||||
* Get difference between original values and current ones, then reset it.
|
* Get difference between original values and current ones, then reset it.
|
||||||
* Similar to call `serializeDiff()` then `resetDiff()`.
|
* Similar to call `serializeDiff()` then `resetDiff()`.
|
||||||
*/
|
*/
|
||||||
save(): Partial<SerializedModel<Shape>>;
|
patch(): Partial<SerializedModel<Shape>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,144 +113,184 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType>
|
||||||
* @param shape Model shape definition.
|
* @param shape Model shape definition.
|
||||||
* @param identifier Identifier property name.
|
* @param identifier Identifier property name.
|
||||||
*/
|
*/
|
||||||
export function model<Shape extends ModelShape, Identifier extends keyof Shape = any>(
|
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends IdentifierNameType<Shape> = any>(
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
identifier?: Identifier,
|
identifier?: Identifier,
|
||||||
): ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>
|
): ModelClass<ModelType, Shape, Identifier>
|
||||||
{
|
{
|
||||||
// Get shape entries.
|
// Get shape entries.
|
||||||
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
|
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
|
||||||
|
|
||||||
return class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>>
|
return withExtends(
|
||||||
{
|
// Initialize generic model class.
|
||||||
constructor()
|
class GenericModel implements ModelDefinition<Shape, IdentifierType<Shape, Identifier>, ModelType>
|
||||||
{
|
{
|
||||||
// Initialize properties to undefined.
|
constructor()
|
||||||
Object.assign(this,
|
{
|
||||||
// Build empty properties model from shape entries.
|
// Initialize properties to undefined.
|
||||||
Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel<Shape>
|
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.
|
* Calling a function for each defined property.
|
||||||
* @param callback - The function to call.
|
* @param callback - The function to call.
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
|
protected forEachModelProperty<ReturnType>(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
|
||||||
{
|
{
|
||||||
for (const [propertyName, propertyDefinition] of shapeEntries)
|
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.
|
||||||
// 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 there is a return value, returning it directly (loop is broken).
|
||||||
if (typeof result !== "undefined") return result;
|
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>;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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];
|
|
||||||
}
|
|
||||||
|
|
||||||
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>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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] = (this as PropertiesModel<Shape>)[propertyName];
|
|
||||||
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): 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<Model<Shape, IdentifierType<Shape, Identifier>>>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,40 @@ export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<Ser
|
||||||
// Reset diff of all elements.
|
// Reset diff of all elements.
|
||||||
value.forEach((value) => this.valueDefinition.type.resetDiff(value));
|
value.forEach((value) => this.valueDefinition.type.resetDiff(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
propertyHasChanged(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;
|
||||||
|
|
||||||
|
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]))
|
||||||
|
// The value has changed, the array is different.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // No change detected.
|
||||||
|
}
|
||||||
|
|
||||||
|
serializedPropertyHasChanged(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;
|
||||||
|
|
||||||
|
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]))
|
||||||
|
// The value has changed, the array is different.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // No change detected.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,11 @@ export class DateType extends Type<string, Date>
|
||||||
|
|
||||||
return value?.toISOString();
|
return value?.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
propertyHasChanged(originalValue: Date|null|undefined, currentValue: Date|null|undefined): boolean
|
||||||
|
{
|
||||||
|
return originalValue?.toISOString() != currentValue?.toISOString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -48,6 +48,15 @@ export class ModelType<Shape extends ModelShape> extends Type<SerializedModel<Sh
|
||||||
// Reset diff of the given model.
|
// Reset diff of the given model.
|
||||||
value?.resetDiff();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -66,6 +66,46 @@ export class ObjectType<Shape extends ModelShape> extends Type<SerializedModel<S
|
||||||
fieldDefinition.type.resetDiff(value?.[fieldName]);
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,6 +9,11 @@ class Author extends s.model({
|
||||||
email: s.property.string(),
|
email: s.property.string(),
|
||||||
createdAt: s.property.date(),
|
createdAt: s.property.date(),
|
||||||
active: s.property.bool(),
|
active: s.property.bool(),
|
||||||
|
}).extends({
|
||||||
|
extension(): string
|
||||||
|
{
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
active: boolean = true;
|
active: boolean = true;
|
||||||
|
@ -104,7 +109,7 @@ it("create and check state then serialize", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("deserialize then save", () => {
|
it("deserialize then patch", () => {
|
||||||
const article = (new Article()).deserialize({
|
const article = (new Article()).deserialize({
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "this is a test",
|
title: "this is a test",
|
||||||
|
@ -125,35 +130,39 @@ it("deserialize then save", () => {
|
||||||
|
|
||||||
expect(article.isDirty()).toBeTruthy();
|
expect(article.isDirty()).toBeTruthy();
|
||||||
|
|
||||||
expect(article.save()).toStrictEqual({
|
expect(article.patch()).toStrictEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
text: "Modified text.",
|
text: "Modified text.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("save with modified submodels", () => {
|
it("patch with modified submodels", () => {
|
||||||
const article = (new Article()).deserialize({
|
const article = (new Article()).deserialize({
|
||||||
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()).toISOString(), 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()).toISOString(), active: false, },
|
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date("1997-09-09")).toISOString(), active: false, },
|
||||||
],
|
],
|
||||||
text: "this is a long test.",
|
text: "this is a long test.",
|
||||||
evaluation: "25.23",
|
evaluation: "25.23",
|
||||||
tags: [ {name: "test"}, {name: "foo"} ],
|
tags: [ {name: "test"}, {name: "foo"} ],
|
||||||
});
|
});
|
||||||
|
|
||||||
article.authors = article.authors.map((author) => {
|
article.authors[0].name = "TEST";
|
||||||
author.name = "TEST";
|
article.authors[1].createdAt.setMonth(9);
|
||||||
return author;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(article.save()).toStrictEqual({
|
expect(article.patch()).toStrictEqual({
|
||||||
id: 1,
|
id: 1,
|
||||||
authors: [
|
authors: [
|
||||||
{ name: "TEST", },
|
{ name: "TEST" },
|
||||||
{}, //{ name: "TEST", firstName: "Another", email: "another@test.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");
|
||||||
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
@ -24,6 +25,6 @@
|
||||||
"lib": [
|
"lib": [
|
||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue