From 4eb8b7d3bc33025a44cc72b830f691b89f19c14c Mon Sep 17 00:00:00 2001
From: Madeorsk
Date: Fri, 4 Oct 2024 21:24:11 +0200
Subject: [PATCH] Add models extension system.
---
README.md | 2 +-
package.json | 2 +-
src/Model/Model.ts | 305 +++++++++++++++++++++++++-------------------
tests/Model.test.ts | 11 ++
tsconfig.json | 3 +-
5 files changed, 190 insertions(+), 133 deletions(-)
diff --git a/README.md b/README.md
index 0385eeb..4d8bd75 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
-
+
## Introduction
diff --git a/package.json b/package.json
index 8a7e0cf..08685e8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@sharkitek/core",
- "version": "3.0.2",
+ "version": "3.1.0",
"description": "TypeScript library for well-designed model architectures.",
"keywords": [
"deserialization",
diff --git a/src/Model/Model.ts b/src/Model/Model.ts
index 9253549..d38bdef 100644
--- a/src/Model/Model.ts
+++ b/src/Model/Model.ts
@@ -34,10 +34,20 @@ export type SerializedModel = {
*/
export type Model = ModelDefinition & PropertiesModel;
+/**
+ * Type of the extends function of model classes.
+ */
+export type ExtendsFunctionType>, Shape extends ModelShape, Identifier extends keyof Shape = any> =
+ (extension: ThisType & Extension) => ModelClass;
+
/**
* Type of a model class.
*/
-export type ModelClass = ConstructorOf>>;
+export type ModelClass>, Shape extends ModelShape, Identifier extends keyof Shape = any> = (
+ ConstructorOf & {
+ extends: ExtendsFunctionType;
+ }
+);
/**
* Identifier type.
@@ -96,141 +106,176 @@ export interface ModelDefinition>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
shape: Shape,
identifier?: Identifier,
-): ConstructorOf
+): ModelClass
{
// Get shape entries.
const shapeEntries = Object.entries(shape) as [keyof Shape, UnknownDefinition][];
- return class GenericModel implements ModelDefinition, ModelType>
- {
- constructor()
+ return withExtends(
+ // Initialize generic model class.
+ class GenericModel implements ModelDefinition, ModelType>
{
- // Initialize properties to undefined.
- Object.assign(this,
- // Build empty properties model from shape entries.
- Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel
- );
- }
+ constructor()
+ {
+ // Initialize properties to undefined.
+ Object.assign(this,
+ // Build empty properties model from shape entries.
+ Object.fromEntries(shapeEntries.map(([key]) => [key, undefined])) as PropertiesModel
+ );
+ }
- /**
- * Calling a function for each defined property.
- * @param callback - The function to call.
- * @protected
- */
- protected forEachModelProperty(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
- {
- for (const [propertyName, propertyDefinition] of shapeEntries)
- { // 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.
- const result = callback(propertyName, propertyDefinition);
- // If there is a return value, returning it directly (loop is broken).
- if (typeof result !== "undefined") return result;
+ /**
+ * Calling a function for each defined property.
+ * @param callback - The function to call.
+ * @protected
+ */
+ protected forEachModelProperty(callback: (propertyName: keyof Shape, propertyDefinition: UnknownDefinition) => ReturnType): ReturnType
+ {
+ for (const [propertyName, propertyDefinition] of shapeEntries)
+ { // 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.
+ const result = callback(propertyName, propertyDefinition);
+ // If there is a return value, returning it directly (loop is broken).
+ if (typeof result !== "undefined") return result;
+ }
+ }
+
+
+ /**
+ * The original properties values.
+ * @protected
+ */
+ protected _originalProperties: Partial> = {};
+
+ /**
+ * The original (serialized) object.
+ * @protected
+ */
+ protected _originalObject: SerializedModel|null = null;
+
+
+
+ getIdentifier(): IdentifierType
+ {
+ return (this as PropertiesModel)?.[identifier];
+ }
+
+ serialize(): SerializedModel
+ {
+ // Creating an empty (=> partial) serialized object.
+ const serializedObject: Partial> = {};
+
+ this.forEachModelProperty((propertyName, propertyDefinition) => {
+ // For each defined model property, adding it to the serialized object.
+ serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel)?.[propertyName]);
+ });
+
+ return serializedObject as SerializedModel; // Returning the serialized object.
+ }
+
+ deserialize(obj: SerializedModel): ModelType
+ {
+ this.forEachModelProperty((propertyName, propertyDefinition) => {
+ // For each defined model property, assigning its deserialized value.
+ (this as PropertiesModel)[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)[propertyName])
+ // There is a difference, we should return false.
+ ? true
+ // There is no difference, returning nothing.
+ : undefined
+ )) === true;
+ }
+
+
+ serializeDiff(): Partial>
+ {
+ // Creating an empty (=> partial) serialized object.
+ const serializedObject: Partial> = {};
+
+ 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)[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)?.[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)[propertyName];
+ propertyDefinition.type.resetDiff((this as PropertiesModel)[propertyName]);
+ });
+ }
+
+ save(): Partial>
+ {
+ // 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
+ );
+}
+
+/**
+ * Any Sharkitek model.
+ */
+export type AnyModel = Model;
+/**
+ * Any Sharkitek model class.
+ */
+export type AnyModelClass = ModelClass;
+
+/**
+ * Add extends function to a model class.
+ * @param genericModel The model class on which to add the extends function.
+ */
+function withExtends>, Shape extends ModelShape, Identifier extends keyof Shape = any>(
+ genericModel: ConstructorOf
+): ModelClass
+{
+ return Object.assign(
+ genericModel,
+ { // Extends function definition.
+ extends(extension: Extension): ModelClass
+ {
+ // Clone the model class and add extends function.
+ const classClone = withExtends(class extends (genericModel as AnyModelClass) {} as AnyModelClass as ConstructorOf);
+ // Add extension to the model class prototype.
+ Object.assign(classClone.prototype, extension);
+ return classClone;
}
}
-
-
- /**
- * The original properties values.
- * @protected
- */
- protected _originalProperties: Partial> = {};
-
- /**
- * The original (serialized) object.
- * @protected
- */
- protected _originalObject: SerializedModel|null = null;
-
-
-
- getIdentifier(): IdentifierType
- {
- return (this as PropertiesModel)?.[identifier];
- }
-
- serialize(): SerializedModel
- {
- // Creating an empty (=> partial) serialized object.
- const serializedObject: Partial> = {};
-
- this.forEachModelProperty((propertyName, propertyDefinition) => {
- // For each defined model property, adding it to the serialized object.
- serializedObject[propertyName] = propertyDefinition.type.serialize((this as PropertiesModel)?.[propertyName]);
- });
-
- return serializedObject as SerializedModel; // Returning the serialized object.
- }
-
- deserialize(obj: SerializedModel): ModelType
- {
- this.forEachModelProperty((propertyName, propertyDefinition) => {
- // For each defined model property, assigning its deserialized value.
- (this as PropertiesModel)[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)[propertyName])
- // There is a difference, we should return false.
- ? true
- // There is no difference, returning nothing.
- : undefined
- )) === true;
- }
-
-
- serializeDiff(): Partial>
- {
- // Creating an empty (=> partial) serialized object.
- const serializedObject: Partial> = {};
-
- 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)[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)?.[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)[propertyName];
- propertyDefinition.type.resetDiff((this as PropertiesModel)[propertyName]);
- });
- }
-
- save(): Partial>
- {
- // 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;
+ ) as AnyModelClass as ModelClass;
}
diff --git a/tests/Model.test.ts b/tests/Model.test.ts
index 44980a1..36aa9b9 100644
--- a/tests/Model.test.ts
+++ b/tests/Model.test.ts
@@ -9,6 +9,11 @@ class Author extends s.model({
email: s.property.string(),
createdAt: s.property.date(),
active: s.property.bool(),
+}).extends({
+ extension(): string
+ {
+ return this.name;
+ }
})
{
active: boolean = true;
@@ -157,3 +162,9 @@ it("save with modified submodels", () => {
],
});
});
+
+it("test author extension", () => {
+ const author = new Author();
+ author.name = "test name";
+ expect(author.extension()).toStrictEqual("test name");
+});
diff --git a/tsconfig.json b/tsconfig.json
index 38dfc55..803756c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
"incremental": true,
"sourceMap": true,
"noImplicitAny": true,
+ "noImplicitThis": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
@@ -24,6 +25,6 @@
"lib": [
"ESNext",
"DOM"
- ],
+ ]
}
}