Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
6eee1b709e | |||
8f8dafed5b | |||
ff9cb91f73 | |||
e373efdd0a | |||
4eb8b7d3bc |
9 changed files with 307 additions and 149 deletions
|
@ -19,7 +19,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Version 3.0.2" src="https://img.shields.io/badge/version-3.0.2-blue" />
|
<img alt="Version 3.3.0" src="https://img.shields.io/badge/version-3.3.0-blue" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
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({
|
||||||
|
@ -227,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()`.
|
||||||
|
@ -246,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:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@sharkitek/core",
|
"name": "@sharkitek/core",
|
||||||
"version": "3.0.2",
|
"version": "3.3.0",
|
||||||
"description": "TypeScript library for well-designed model architectures.",
|
"description": "TypeScript library for well-designed model architectures.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deserialization",
|
"deserialization",
|
||||||
|
|
|
@ -34,16 +34,31 @@ 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.
|
* Type of a model class.
|
||||||
*/
|
*/
|
||||||
export type ModelClass<Shape extends ModelShape, Identifier extends keyof Shape = any> = ConstructorOf<Model<Shape, IdentifierType<Shape, Identifier>>>;
|
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.
|
||||||
*/
|
*/
|
||||||
|
@ -54,6 +69,11 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
|
||||||
*/
|
*/
|
||||||
getIdentifier(): IdentifierType;
|
getIdentifier(): IdentifierType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get model identifier name.
|
||||||
|
*/
|
||||||
|
getIdentifierName(): IdentifierNameType<Shape>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize the model.
|
* Serialize the model.
|
||||||
*/
|
*/
|
||||||
|
@ -85,7 +105,7 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
|
||||||
* 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>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,144 +113,184 @@ export interface ModelDefinition<Shape extends ModelShape, IdentifierType, Model
|
||||||
* @param shape Model shape definition.
|
* @param shape Model shape definition.
|
||||||
* @param identifier Identifier property name.
|
* @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,
|
shape: Shape,
|
||||||
identifier?: Identifier,
|
identifier?: Identifier,
|
||||||
): ConstructorOf<ModelType>
|
): 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>, ModelType>
|
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>): 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] = (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<ModelType>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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