diff --git a/README.md b/README.md index 822eb17..1079a62 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,22 @@ Sharkitek is a Javascript / TypeScript library designed to ease development of client-side models. -With Sharkitek, you define the architecture of your models by applying decorators (which define their type) on your class properties. +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` or `serializeDiff`. -Sharkitek makes use of decorators as defined in the [TypeScript Reference](https://www.typescriptlang.org/docs/handbook/decorators.html). -Due to the way decorators work, you must always set a value to your properties when you declare them, even if this value is `undefined`. - ```typescript -class Example extends Model +class Example extends Model { - @Property(SNumeric) - @Identifier - id: number = undefined; + id: number; + name: string; - @Property(SString) - name: string = undefined; + protected SDefinition(): ModelDefinition + { + return { + id: SDefine(SNumeric), + name: SDefine(SString), + }; + } } ``` @@ -30,53 +31,60 @@ class Example extends Model /** * A person. */ -class Person extends Model +class Person extends Model { - @Property(SNumeric) - @Identifier - id: number = undefined; - - @Property(SString) - name: string = undefined; - - @Property(SString) - firstName: string = undefined; - - @Property(SString) - email: string = undefined; - - @Property(SDate) - createdAt: Date = undefined; - - @Property(SBool) + id: number; + name: string; + firstName: string; + email: string; + createdAt: Date; active: boolean = true; + + protected SIdentifier(): ModelIdentifier + { + return "id"; + } + + protected SDefinition(): ModelDefinition + { + return { + name: SDefine(SString), + firstName: SDefine(SString), + email: SDefine(SString), + createdAt: SDefine(SDate), + active: SDefine(SBool), + }; + } } ``` -**Important**: You _must_ set a value to all your defined properties. If there is no set value, the decorator will not -be applied instantly on object initialization and the deserialization will not work properly. - ```typescript /** * An article. */ -class Article extends Model +class Article extends Model
{ - @Property(SNumeric) - @Identifier - id: number = undefined; - - @Property(SString) - title: string = undefined; - - @Property(SArray(SModel(Author))) + id: number; + title: string; authors: Author[] = []; + text: string; + evaluation: number; - @Property(SString) - text: string = undefined; + protected SIdentifier(): ModelIdentifier
+ { + return "id"; + } - @Property(SDecimal) - evaluation: number = undefined; + protected SDefinition(): ModelDefinition
+ { + return { + id: SDefine(SNumeric), + title: SDefine(SString), + authors: SDefine(SArray(SModel(Author))), + text: SDefine(SString), + evaluation: SDefine(SDecimal), + }; + } } ``` @@ -99,10 +107,16 @@ Sharkitek defines some basic types by default, in these classes: When you are defining a Sharkitek property, you must provide its type by instantiating one of these classes. ```typescript -class Example extends Model +class Example extends Model { - @Property(new StringType()) - foo: string = undefined; + foo: string; + + protected SDefinition(): ModelDefinition + { + return { + foo: new Definition(new StringType()), + }; + } } ``` @@ -125,10 +139,16 @@ multiple functions or constants when predefined parameters. (For example, we cou be a variable similar to `SArray(SString)`.) ```typescript -class Example extends Model +class Example extends Model { - @Property(SString) foo: string = undefined; + + protected SDefinition(): ModelDefinition + { + return { + foo: SDefine(SString), + }; + } } ``` diff --git a/package.json b/package.json index c327b52..8fe80df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sharkitek/core", - "version": "1.3.1", + "version": "2.0.0", "description": "Sharkitek core models library.", "keywords": [ "sharkitek", @@ -24,9 +24,6 @@ "files": [ "lib/**/*" ], - "dependencies": { - "reflect-metadata": "^0.1.13" - }, "devDependencies": { "@types/jest": "^28.1.6", "esbuild": "^0.15.8", diff --git a/src/Model/Definition.ts b/src/Model/Definition.ts new file mode 100644 index 0000000..b727bac --- /dev/null +++ b/src/Model/Definition.ts @@ -0,0 +1,34 @@ +import {Type} from "./Types/Type"; + +/** + * Options of a definition. + */ +export interface DefinitionOptions +{ //TODO implement some options, like `mandatory`. +} + +/** + * A Sharkitek model property definition. + */ +export class Definition +{ + /** + * Initialize a property definition with the given type and options. + * @param type - The model property type. + * @param options - Property definition options. + */ + constructor( + public type: Type, + public options: DefinitionOptions = {}, + ) {} +} + +/** + * Initialize a property definition with the given type and options. + * @param type - The model property type. + * @param options - Property definition options. + */ +export function SDefine(type: Type, options: DefinitionOptions = {}) +{ + return new Definition(type, options); +} diff --git a/src/Model/Model.ts b/src/Model/Model.ts index f3a0f18..a63664a 100644 --- a/src/Model/Model.ts +++ b/src/Model/Model.ts @@ -1,89 +1,50 @@ -import {Type} from "./Types/Type"; -import "reflect-metadata"; -import {ConstructorOf} from "./Types/ModelType"; +import {Definition} from "./Definition"; /** - * Key of Sharkitek property metadata. + * Model properties definition type. */ -const sharkitekMetadataKey = Symbol("sharkitek"); - +export type ModelDefinition = Partial>>; /** - * Key of Sharkitek model identifier. + * Model identifier type. */ -const modelIdentifierMetadataKey = Symbol("modelIdentifier"); - -/** - * Sharkitek property metadata interface. - */ -interface SharkitekMetadataInterface -{ - /** - * Property type instance. - */ - type: Type; -} - -/** - * Class decorator for Sharkitek models. - */ -export function Sharkitek(constructor: Function) -{ - /*return class extends (constructor as FunctionConstructor) { - constructor() - { - super(); - } - };*/ -} - -/** - * Property decorator to define a Sharkitek model identifier. - */ -export function Identifier(obj: Model, propertyName: string): void -{ - // Register the current property as identifier of the current model object. - Reflect.defineMetadata(modelIdentifierMetadataKey, propertyName, obj); -} - -/** - * Property decorator for Sharkitek models properties. - * @param type - Type of the property. - */ -export function Property(type: Type): PropertyDecorator -{ - // Return the decorator function. - return (obj: ConstructorOf, propertyName) => { - // Initializing property metadata. - const metadata: SharkitekMetadataInterface = { - type: type, - }; - // Set property metadata. - Reflect.defineMetadata(sharkitekMetadataKey, metadata, obj, propertyName); - }; -} +export type ModelIdentifier = keyof T; /** * A Sharkitek model. */ -export abstract class Model +export abstract class Model { /** - * Get the Sharkitek model identifier. - * @private + * Model properties definition function. */ - private getModelIdentifier(): string - { - return Reflect.getMetadata(modelIdentifierMetadataKey, this); - } + protected abstract SDefinition(): ModelDefinition; + /** - * Get the Sharkitek metadata of the property. - * @param propertyName - The name of the property for which to get metadata. - * @private + * Return the name of the model identifier property. */ - private getPropertyMetadata(propertyName: string): SharkitekMetadataInterface + protected SIdentifier(): ModelIdentifier { - return Reflect.getMetadata(sharkitekMetadataKey, this, propertyName); + return undefined; } + + /** + * Get given property definition. + * @protected + */ + protected getPropertyDefinition(propertyName: string): Definition + { + return (this.SDefinition() as any)?.[propertyName]; + } + + /** + * Get the list of the model properties. + * @protected + */ + protected getProperties(): string[] + { + return Object.keys(this.SDefinition()); + } + /** * Calling a function for a defined property. * @param propertyName - The property for which to check definition. @@ -91,15 +52,15 @@ export abstract class Model * @param notProperty - The function called when the property is not defined. * @protected */ - protected propertyWithMetadata(propertyName: string, callback: (propertyMetadata: SharkitekMetadataInterface) => void, notProperty: () => void = () => {}): unknown + protected propertyWithDefinition(propertyName: string, callback: (propertyDefinition: Definition) => void, notProperty: () => void = () => {}): unknown { - // Getting the current property metadata. - const propertyMetadata = this.getPropertyMetadata(propertyName); - if (propertyMetadata) - // Metadata are defined, calling the right callback. - return callback(propertyMetadata); + // Getting the current property definition. + const propertyDefinition = this.getPropertyDefinition(propertyName); + if (propertyDefinition) + // There is a definition for the current property, calling the right callback. + return callback(propertyDefinition); else - // Metadata are not defined, calling the right callback. + // No definition for the given property, calling the right callback. return notProperty(); } /** @@ -107,19 +68,16 @@ export abstract class Model * @param callback - The function to call. * @protected */ - protected forEachModelProperty(callback: (propertyName: string, propertyMetadata: SharkitekMetadataInterface) => unknown): any|void + protected forEachModelProperty(callback: (propertyName: string, propertyDefinition: Definition) => unknown): any|void { - for (const propertyName of Object.keys(this)) + for (const propertyName of this.getProperties()) { // For each property, checking that its type is defined and calling the callback with its type. - const result = this.propertyWithMetadata(propertyName, (propertyMetadata) => { - // If the property is defined, calling the function with the property name and metadata. - const result = callback(propertyName, propertyMetadata); + const result = this.propertyWithDefinition(propertyName, (propertyDefinition) => { + // 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; - - // Update metadata if they have changed. - Reflect.defineMetadata(sharkitekMetadataKey, propertyMetadata, this, propertyName); }); // If there is a return value, returning it directly (loop is broken). @@ -153,12 +111,12 @@ export abstract class Model */ isDirty(): boolean { - return this.forEachModelProperty((propertyName, propertyMetadata) => ( + return this.forEachModelProperty((propertyName, propertyDefinition) => ( // For each property, checking if it is different. - propertyMetadata.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]) + propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]) // There is a difference, we should return false. ? true - // There is not difference, returning nothing. + // There is no difference, returning nothing. : undefined )) === true; } @@ -168,7 +126,7 @@ export abstract class Model */ getIdentifier(): unknown { - return (this as any)[this.getModelIdentifier()]; + return (this as any)[this.SIdentifier()]; } /** @@ -176,10 +134,10 @@ export abstract class Model */ resetDiff() { - this.forEachModelProperty((propertyName, propertyMetadata) => { + this.forEachModelProperty((propertyName, propertyDefinition) => { // For each property, set its original value to its current property value. this._originalProperties[propertyName] = (this as any)[propertyName]; - propertyMetadata.type.resetDiff((this as any)[propertyName]); + propertyDefinition.type.resetDiff((this as any)[propertyName]); }); } /** @@ -190,12 +148,12 @@ export abstract class Model // Creating a serialized object. const serializedDiff: any = {}; - this.forEachModelProperty((propertyName, propertyMetadata) => { + this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, adding it to the serialized object if it has changed. - if (this.getModelIdentifier() == propertyName - || propertyMetadata.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])) + if (this.SIdentifier() == propertyName + || propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])) // Adding the current property to the serialized object if it is the identifier or its value has changed. - serializedDiff[propertyName] = propertyMetadata.type.serializeDiff((this as any)[propertyName]); + serializedDiff[propertyName] = propertyDefinition.type.serializeDiff((this as any)[propertyName]); }) return serializedDiff; // Returning the serialized object. @@ -224,9 +182,9 @@ export abstract class Model // Creating a serialized object. const serializedObject: any = {}; - this.forEachModelProperty((propertyName, propertyMetadata) => { + this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, adding it to the serialized object. - serializedObject[propertyName] = propertyMetadata.type.serialize((this as any)[propertyName]); + serializedObject[propertyName] = propertyDefinition.type.serialize((this as any)[propertyName]); }); return serializedObject; // Returning the serialized object. @@ -237,16 +195,16 @@ export abstract class Model * @protected */ protected parse(): void - {} // Nothing by default. TODO: create a event system to create functions like "beforeDeserialization" or "afterDeserialization". + {} // Nothing by default. TODO: create an event system to create functions like "beforeDeserialization" or "afterDeserialization". /** * Deserialize the model. */ - deserialize(serializedObject: any): this + deserialize(serializedObject: any): THIS { - this.forEachModelProperty((propertyName, propertyMetadata) => { + this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, assigning its deserialized value to the model. - (this as any)[propertyName] = propertyMetadata.type.deserialize(serializedObject[propertyName]); + (this as any)[propertyName] = propertyDefinition.type.deserialize(serializedObject[propertyName]); }); // Reset original property values. @@ -254,6 +212,6 @@ export abstract class Model this._originalObject = serializedObject; // The model is not a new one, but loaded from a deserialized one. - return this; // Returning this, after deserialization. + return this as unknown as THIS; // Returning this, after deserialization. } } diff --git a/src/Model/Types/ModelType.ts b/src/Model/Types/ModelType.ts index 31cce58..103bc84 100644 --- a/src/Model/Types/ModelType.ts +++ b/src/Model/Types/ModelType.ts @@ -9,7 +9,7 @@ export type ConstructorOf = { new(): T; } /** * Type of a Sharkitek model value. */ -export class ModelType extends Type +export class ModelType> extends Type { /** * Constructs a new model type of a Sharkitek model property. @@ -49,7 +49,7 @@ export class ModelType extends Type * Type of a Sharkitek model value. * @param modelConstructor - Constructor of the model. */ -export function SModel(modelConstructor: ConstructorOf) +export function SModel>(modelConstructor: ConstructorOf) { return new ModelType(modelConstructor); } diff --git a/src/index.ts b/src/index.ts index 8e0be62..aa82848 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ export * from "./Model/Model"; +export * from "./Model/Definition"; + export * from "./Model/Types/Type"; export * from "./Model/Types/ArrayType"; export * from "./Model/Types/BoolType"; diff --git a/tests/Model.test.ts b/tests/Model.test.ts index 806da3a..ed197e6 100644 --- a/tests/Model.test.ts +++ b/tests/Model.test.ts @@ -1,25 +1,38 @@ -import {SArray, SDecimal, SModel, SNumeric, SString, SDate, SBool, Identifier, Model, Property} from "../src"; +import { + SArray, + SDecimal, + SModel, + SNumeric, + SString, + SDate, + SBool, + Model, + ModelDefinition, + SDefine, ModelIdentifier +} from "../src"; /** * Another test model. */ -class Author extends Model +class Author extends Model { - @Property(SString) - name: string = undefined; - - @Property(SString) - firstName: string = undefined; - - @Property(SString) - email: string = undefined; - - @Property(SDate) - createdAt: Date = undefined; - - @Property(SBool) + name: string; + firstName: string; + email: string; + createdAt: Date; active: boolean = true; + protected SDefinition(): ModelDefinition + { + return { + name: SDefine(SString), + firstName: SDefine(SString), + email: SDefine(SString), + createdAt: SDefine(SDate), + active: SDefine(SBool), + }; + } + constructor(name: string = undefined, firstName: string = undefined, email: string = undefined, createdAt: Date = undefined) { super(); @@ -34,23 +47,29 @@ class Author extends Model /** * A test model. */ -class Article extends Model +class Article extends Model
{ - @Property(SNumeric) - @Identifier - id: number = undefined; - - @Property(SString) - title: string = undefined; - - @Property(SArray(SModel(Author))) + id: number; + title: string; authors: Author[] = []; + text: string; + evaluation: number; - @Property(SString) - text: string = undefined; + protected SIdentifier(): ModelIdentifier
+ { + return "id"; + } - @Property(SDecimal) - evaluation: number = undefined; + protected SDefinition(): ModelDefinition
+ { + return { + id: SDefine(SNumeric), + title: SDefine(SString), + authors: SDefine(SArray(SModel(Author))), + text: SDefine(SString), + evaluation: SDefine(SDecimal), + }; + } } it("deserialize", () => { diff --git a/tsconfig.json b/tsconfig.json index 62aa652..af1362b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,6 @@ "outDir": "./lib/", "noImplicitAny": true, "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, "declaration": true, "declarationMap": true, "sourceMap": true,