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 @@

- Version 3.0.2 + Version 3.1.0

## 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" - ], + ] } }