Architecture simplification: stop using decorators which adds unnecessary complexity and some compilation bugs.

+ Add a property definition class.
+ Add some definition functions on models, which have to be redefined when implementing a new model.
- Remove decorators.
This commit is contained in:
Madeorsk 2022-11-01 19:13:21 +01:00
parent 13072b453f
commit 1c3c87a4a6
8 changed files with 215 additions and 187 deletions

118
README.md
View file

@ -4,21 +4,22 @@
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 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`. 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 ```typescript
class Example extends Model class Example extends Model<Example>
{ {
@Property(SNumeric) id: number;
@Identifier name: string;
id: number = undefined;
@Property(SString) protected SDefinition(): ModelDefinition<Example>
name: string = undefined; {
return {
id: SDefine(SNumeric),
name: SDefine(SString),
};
}
} }
``` ```
@ -30,53 +31,60 @@ class Example extends Model
/** /**
* A person. * A person.
*/ */
class Person extends Model class Person extends Model<Person>
{ {
@Property(SNumeric) id: number;
@Identifier name: string;
id: number = undefined; firstName: string;
email: string;
@Property(SString) createdAt: Date;
name: string = undefined;
@Property(SString)
firstName: string = undefined;
@Property(SString)
email: string = undefined;
@Property(SDate)
createdAt: Date = undefined;
@Property(SBool)
active: boolean = true; active: boolean = true;
protected SIdentifier(): ModelIdentifier<Person>
{
return "id";
}
protected SDefinition(): ModelDefinition<Person>
{
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 ```typescript
/** /**
* An article. * An article.
*/ */
class Article extends Model class Article extends Model<Article>
{ {
@Property(SNumeric) id: number;
@Identifier title: string;
id: number = undefined;
@Property(SString)
title: string = undefined;
@Property(SArray(SModel(Author)))
authors: Author[] = []; authors: Author[] = [];
text: string;
evaluation: number;
@Property(SString) protected SIdentifier(): ModelIdentifier<Article>
text: string = undefined; {
return "id";
}
@Property(SDecimal) protected SDefinition(): ModelDefinition<Article>
evaluation: number = undefined; {
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. When you are defining a Sharkitek property, you must provide its type by instantiating one of these classes.
```typescript ```typescript
class Example extends Model class Example extends Model<Example>
{ {
@Property(new StringType()) foo: string;
foo: string = undefined;
protected SDefinition(): ModelDefinition<Example>
{
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)`.) be a variable similar to `SArray(SString)`.)
```typescript ```typescript
class Example extends Model class Example extends Model<Example>
{ {
@Property(SString)
foo: string = undefined; foo: string = undefined;
protected SDefinition(): ModelDefinition<Example>
{
return {
foo: SDefine(SString),
};
}
} }
``` ```

View file

@ -1,6 +1,6 @@
{ {
"name": "@sharkitek/core", "name": "@sharkitek/core",
"version": "1.3.1", "version": "2.0.0",
"description": "Sharkitek core models library.", "description": "Sharkitek core models library.",
"keywords": [ "keywords": [
"sharkitek", "sharkitek",
@ -24,9 +24,6 @@
"files": [ "files": [
"lib/**/*" "lib/**/*"
], ],
"dependencies": {
"reflect-metadata": "^0.1.13"
},
"devDependencies": { "devDependencies": {
"@types/jest": "^28.1.6", "@types/jest": "^28.1.6",
"esbuild": "^0.15.8", "esbuild": "^0.15.8",

34
src/Model/Definition.ts Normal file
View file

@ -0,0 +1,34 @@
import {Type} from "./Types/Type";
/**
* Options of a definition.
*/
export interface DefinitionOptions<SerializedType, SharkitekType>
{ //TODO implement some options, like `mandatory`.
}
/**
* A Sharkitek model property definition.
*/
export class Definition<SerializedType, SharkitekType>
{
/**
* 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<SerializedType, SharkitekType>,
public options: DefinitionOptions<SerializedType, SharkitekType> = {},
) {}
}
/**
* Initialize a property definition with the given type and options.
* @param type - The model property type.
* @param options - Property definition options.
*/
export function SDefine<SerializedType, SharkitekType>(type: Type<SerializedType, SharkitekType>, options: DefinitionOptions<SerializedType, SharkitekType> = {})
{
return new Definition(type, options);
}

View file

@ -1,89 +1,50 @@
import {Type} from "./Types/Type"; import {Definition} from "./Definition";
import "reflect-metadata";
import {ConstructorOf} from "./Types/ModelType";
/** /**
* Key of Sharkitek property metadata. * Model properties definition type.
*/ */
const sharkitekMetadataKey = Symbol("sharkitek"); export type ModelDefinition<T> = Partial<Record<keyof T, Definition<unknown, unknown>>>;
/** /**
* Key of Sharkitek model identifier. * Model identifier type.
*/ */
const modelIdentifierMetadataKey = Symbol("modelIdentifier"); export type ModelIdentifier<T> = keyof T;
/**
* Sharkitek property metadata interface.
*/
interface SharkitekMetadataInterface
{
/**
* Property type instance.
*/
type: Type<any, any>;
}
/**
* 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<SerializedType, SharkitekType>(type: Type<SerializedType, SharkitekType>): PropertyDecorator
{
// Return the decorator function.
return (obj: ConstructorOf<Model>, propertyName) => {
// Initializing property metadata.
const metadata: SharkitekMetadataInterface = {
type: type,
};
// Set property metadata.
Reflect.defineMetadata(sharkitekMetadataKey, metadata, obj, propertyName);
};
}
/** /**
* A Sharkitek model. * A Sharkitek model.
*/ */
export abstract class Model export abstract class Model<THIS>
{ {
/** /**
* Get the Sharkitek model identifier. * Model properties definition function.
* @private
*/ */
private getModelIdentifier(): string protected abstract SDefinition(): ModelDefinition<THIS>;
{
return Reflect.getMetadata(modelIdentifierMetadataKey, this);
}
/** /**
* Get the Sharkitek metadata of the property. * Return the name of the model identifier property.
* @param propertyName - The name of the property for which to get metadata.
* @private
*/ */
private getPropertyMetadata(propertyName: string): SharkitekMetadataInterface protected SIdentifier(): ModelIdentifier<THIS>
{ {
return Reflect.getMetadata(sharkitekMetadataKey, this, propertyName); return undefined;
} }
/**
* Get given property definition.
* @protected
*/
protected getPropertyDefinition(propertyName: string): Definition<unknown, unknown>
{
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. * Calling a function for a defined property.
* @param propertyName - The property for which to check definition. * @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. * @param notProperty - The function called when the property is not defined.
* @protected * @protected
*/ */
protected propertyWithMetadata(propertyName: string, callback: (propertyMetadata: SharkitekMetadataInterface) => void, notProperty: () => void = () => {}): unknown protected propertyWithDefinition(propertyName: string, callback: (propertyDefinition: Definition<unknown, unknown>) => void, notProperty: () => void = () => {}): unknown
{ {
// Getting the current property metadata. // Getting the current property definition.
const propertyMetadata = this.getPropertyMetadata(propertyName); const propertyDefinition = this.getPropertyDefinition(propertyName);
if (propertyMetadata) if (propertyDefinition)
// Metadata are defined, calling the right callback. // There is a definition for the current property, calling the right callback.
return callback(propertyMetadata); return callback(propertyDefinition);
else else
// Metadata are not defined, calling the right callback. // No definition for the given property, calling the right callback.
return notProperty(); return notProperty();
} }
/** /**
@ -107,19 +68,16 @@ export abstract class Model
* @param callback - The function to call. * @param callback - The function to call.
* @protected * @protected
*/ */
protected forEachModelProperty(callback: (propertyName: string, propertyMetadata: SharkitekMetadataInterface) => unknown): any|void protected forEachModelProperty(callback: (propertyName: string, propertyDefinition: Definition<unknown, unknown>) => 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. { // For each property, checking that its type is defined and calling the callback with its type.
const result = this.propertyWithMetadata(propertyName, (propertyMetadata) => { const result = this.propertyWithDefinition(propertyName, (propertyDefinition) => {
// If the property is defined, calling the function with the property name and metadata. // If the property is defined, calling the function with the property name and definition.
const result = callback(propertyName, propertyMetadata); 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;
// Update metadata if they have changed.
Reflect.defineMetadata(sharkitekMetadataKey, propertyMetadata, this, propertyName);
}); });
// If there is a return value, returning it directly (loop is broken). // If there is a return value, returning it directly (loop is broken).
@ -153,12 +111,12 @@ export abstract class Model
*/ */
isDirty(): boolean isDirty(): boolean
{ {
return this.forEachModelProperty((propertyName, propertyMetadata) => ( return this.forEachModelProperty((propertyName, propertyDefinition) => (
// For each property, checking if it is different. // 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. // There is a difference, we should return false.
? true ? true
// There is not difference, returning nothing. // There is no difference, returning nothing.
: undefined : undefined
)) === true; )) === true;
} }
@ -168,7 +126,7 @@ export abstract class Model
*/ */
getIdentifier(): unknown getIdentifier(): unknown
{ {
return (this as any)[this.getModelIdentifier()]; return (this as any)[this.SIdentifier()];
} }
/** /**
@ -176,10 +134,10 @@ export abstract class Model
*/ */
resetDiff() resetDiff()
{ {
this.forEachModelProperty((propertyName, propertyMetadata) => { this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each property, set its original value to its current property value. // For each property, set its original value to its current property value.
this._originalProperties[propertyName] = (this as any)[propertyName]; 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. // Creating a serialized object.
const serializedDiff: any = {}; 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. // For each defined model property, adding it to the serialized object if it has changed.
if (this.getModelIdentifier() == propertyName if (this.SIdentifier() == propertyName
|| propertyMetadata.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[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. // 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. return serializedDiff; // Returning the serialized object.
@ -224,9 +182,9 @@ export abstract class Model
// Creating a serialized object. // Creating a serialized object.
const serializedObject: any = {}; const serializedObject: any = {};
this.forEachModelProperty((propertyName, propertyMetadata) => { this.forEachModelProperty((propertyName, propertyDefinition) => {
// For each defined model property, adding it to the serialized object. // 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. return serializedObject; // Returning the serialized object.
@ -237,16 +195,16 @@ export abstract class Model
* @protected * @protected
*/ */
protected parse(): void 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 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. // 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. // 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. 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.
} }
} }

View file

@ -9,7 +9,7 @@ export type ConstructorOf<T> = { new(): T; }
/** /**
* Type of a Sharkitek model value. * Type of a Sharkitek model value.
*/ */
export class ModelType<M extends Model> extends Type<any, M> export class ModelType<M extends Model<M>> extends Type<any, M>
{ {
/** /**
* Constructs a new model type of a Sharkitek model property. * Constructs a new model type of a Sharkitek model property.
@ -49,7 +49,7 @@ export class ModelType<M extends Model> extends Type<any, M>
* Type of a Sharkitek model value. * Type of a Sharkitek model value.
* @param modelConstructor - Constructor of the model. * @param modelConstructor - Constructor of the model.
*/ */
export function SModel<M extends Model>(modelConstructor: ConstructorOf<M>) export function SModel<M extends Model<M>>(modelConstructor: ConstructorOf<M>)
{ {
return new ModelType(modelConstructor); return new ModelType(modelConstructor);
} }

View file

@ -2,6 +2,8 @@
export * from "./Model/Model"; export * from "./Model/Model";
export * from "./Model/Definition";
export * from "./Model/Types/Type"; export * from "./Model/Types/Type";
export * from "./Model/Types/ArrayType"; export * from "./Model/Types/ArrayType";
export * from "./Model/Types/BoolType"; export * from "./Model/Types/BoolType";

View file

@ -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. * Another test model.
*/ */
class Author extends Model class Author extends Model<Author>
{ {
@Property(SString) name: string;
name: string = undefined; firstName: string;
email: string;
@Property(SString) createdAt: Date;
firstName: string = undefined;
@Property(SString)
email: string = undefined;
@Property(SDate)
createdAt: Date = undefined;
@Property(SBool)
active: boolean = true; active: boolean = true;
protected SDefinition(): ModelDefinition<Author>
{
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) constructor(name: string = undefined, firstName: string = undefined, email: string = undefined, createdAt: Date = undefined)
{ {
super(); super();
@ -34,23 +47,29 @@ class Author extends Model
/** /**
* A test model. * A test model.
*/ */
class Article extends Model class Article extends Model<Article>
{ {
@Property(SNumeric) id: number;
@Identifier title: string;
id: number = undefined;
@Property(SString)
title: string = undefined;
@Property(SArray(SModel(Author)))
authors: Author[] = []; authors: Author[] = [];
text: string;
evaluation: number;
@Property(SString) protected SIdentifier(): ModelIdentifier<Article>
text: string = undefined; {
return "id";
}
@Property(SDecimal) protected SDefinition(): ModelDefinition<Article>
evaluation: number = undefined; {
return {
id: SDefine(SNumeric),
title: SDefine(SString),
authors: SDefine(SArray(SModel(Author))),
text: SDefine(SString),
evaluation: SDefine(SDecimal),
};
}
} }
it("deserialize", () => { it("deserialize", () => {

View file

@ -10,8 +10,6 @@
"outDir": "./lib/", "outDir": "./lib/",
"noImplicitAny": true, "noImplicitAny": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,