Compare commits

...

4 commits
v3.1.0 ... main

8 changed files with 121 additions and 20 deletions

View file

@ -19,7 +19,7 @@
</p>
<p align="center">
<img alt="Version 3.1.0" src="https://img.shields.io/badge/version-3.1.0-blue" />
<img alt="Version 3.3.0" src="https://img.shields.io/badge/version-3.3.0-blue" />
</p>
## Introduction
@ -27,7 +27,7 @@
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.
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
class Example extends s.model({
@ -227,7 +227,7 @@ const result = model.serializeDiff();
// result = {}
```
#### `save()`
#### `patch()`
Get difference between original values and current ones, then reset it.
Similar to call `serializeDiff()` then `resetDiff()`.
@ -246,7 +246,7 @@ const model = (new TestModel()).deserialize({
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:
// result = { id: 5, title: "A new title for a new world" }
// if `id` is not defined as the model identifier:

View file

@ -1,6 +1,6 @@
{
"name": "@sharkitek/core",
"version": "3.1.0",
"version": "3.3.0",
"description": "TypeScript library for well-designed model architectures.",
"keywords": [
"deserialization",

View file

@ -54,6 +54,11 @@ export type ModelClass<ModelType extends Model<Shape, IdentifierType<Shape, Iden
*/
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.
*/
@ -64,6 +69,11 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
*/
getIdentifier(): IdentifierType;
/**
* Get model identifier name.
*/
getIdentifierName(): IdentifierNameType<Shape>;
/**
* Serialize the model.
*/
@ -95,7 +105,7 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
* Get difference between original values and current ones, then reset it.
* Similar to call `serializeDiff()` then `resetDiff()`.
*/
save(): Partial<SerializedModel<Shape>>;
patch(): Partial<SerializedModel<Shape>>;
}
/**
@ -103,7 +113,7 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
* @param shape Model shape definition.
* @param identifier Identifier property name.
*/
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
export function model<ModelType extends Model<Shape, IdentifierType<Shape, Identifier>>, Shape extends ModelShape, Identifier extends IdentifierNameType<Shape> = any>(
shape: Shape,
identifier?: Identifier,
): ModelClass<ModelType, Shape, Identifier>
@ -160,6 +170,11 @@ export function model<ModelType extends Model<Shape, IdentifierType<Shape, Ident
return (this as PropertiesModel<Shape>)?.[identifier];
}
getIdentifierName(): IdentifierNameType<Shape>
{
return identifier;
}
serialize(): SerializedModel<Shape>
{
// Creating an empty (=> partial) serialized object.
@ -228,12 +243,12 @@ export function model<ModelType extends Model<Shape, IdentifierType<Shape, Ident
{
this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as PropertiesModel<Shape>)[propertyName];
this._originalProperties[propertyName] = structuredClone(this as PropertiesModel<Shape>)[propertyName];
propertyDefinition.type.resetDiff((this as PropertiesModel<Shape>)[propertyName]);
});
}
save(): Partial<SerializedModel<Shape>>
patch(): Partial<SerializedModel<Shape>>
{
// Get the difference.
const diff = this.serializeDiff();

View file

@ -54,6 +54,40 @@ export class ArrayType<SerializedValueType, SharkitekValueType> extends Type<Ser
// Reset diff of all elements.
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.
}
}
/**

View file

@ -21,6 +21,11 @@ export class DateType extends Type<string, Date>
return value?.toISOString();
}
propertyHasChanged(originalValue: Date|null|undefined, currentValue: Date|null|undefined): boolean
{
return originalValue?.toISOString() != currentValue?.toISOString();
}
}
/**

View file

@ -48,6 +48,15 @@ export class ModelType<Shape extends ModelShape> extends Type<SerializedModel<Sh
// Reset diff of the given model.
value?.resetDiff();
}
propertyHasChanged(originalValue: Model<Shape>|null|undefined, currentValue: Model<Shape>|null|undefined): boolean
{
if (originalValue === undefined) return currentValue !== undefined;
if (originalValue === null) return currentValue !== null;
// If the current value is dirty, property has changed.
return currentValue.isDirty();
}
}
/**

View file

@ -66,6 +66,46 @@ export class ObjectType<Shape extends ModelShape> extends Type<SerializedModel<S
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.
}
}
/**

View file

@ -109,7 +109,7 @@ it("create and check state then serialize", () => {
});
it("deserialize then save", () => {
it("deserialize then patch", () => {
const article = (new Article()).deserialize({
id: 1,
title: "this is a test",
@ -130,35 +130,33 @@ it("deserialize then save", () => {
expect(article.isDirty()).toBeTruthy();
expect(article.save()).toStrictEqual({
expect(article.patch()).toStrictEqual({
id: 1,
text: "Modified text.",
});
});
it("save with modified submodels", () => {
it("patch with modified submodels", () => {
const article = (new Article()).deserialize({
id: 1,
title: "this is a test",
authors: [
{ name: "DOE", firstName: "John", email: "test@test.test", createdAt: (new Date()).toISOString(), active: true, },
{ name: "TEST", firstName: "Another", email: "another@test.test", createdAt: (new Date()).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.",
evaluation: "25.23",
tags: [ {name: "test"}, {name: "foo"} ],
});
article.authors = article.authors.map((author) => {
author.name = "TEST";
return author;
});
article.authors[0].name = "TEST";
article.authors[1].createdAt.setMonth(9);
expect(article.save()).toStrictEqual({
expect(article.patch()).toStrictEqual({
id: 1,
authors: [
{ name: "TEST", },
{}, //{ name: "TEST", firstName: "Another", email: "another@test.test" },
{ name: "TEST" },
{ createdAt: (new Date("1997-10-09")).toISOString() }, //{ name: "TEST", firstName: "Another", email: "another@test.test" },
],
});
});