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.
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<Example>
{
@Property(SNumeric)
@Identifier
id: number = undefined;
id: number;
name: string;
@Property(SString)
name: string = undefined;
protected SDefinition(): ModelDefinition<Example>
{
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<Person>
{
@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<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
/**
* An article.
*/
class Article extends Model
class Article extends Model<Article>
{
@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<Article>
{
return "id";
}
@Property(SDecimal)
evaluation: number = undefined;
protected SDefinition(): ModelDefinition<Article>
{
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<Example>
{
@Property(new StringType())
foo: string = undefined;
foo: string;
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)`.)
```typescript
class Example extends Model
class Example extends Model<Example>
{
@Property(SString)
foo: string = undefined;
protected SDefinition(): ModelDefinition<Example>
{
return {
foo: SDefine(SString),
};
}
}
```

View File

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

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 "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<T> = Partial<Record<keyof T, Definition<unknown, unknown>>>;
/**
* Key of Sharkitek model identifier.
* Model identifier type.
*/
const modelIdentifierMetadataKey = Symbol("modelIdentifier");
/**
* 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);
};
}
export type ModelIdentifier<T> = keyof T;
/**
* A Sharkitek model.
*/
export abstract class Model
export abstract class Model<THIS>
{
/**
* Get the Sharkitek model identifier.
* @private
* Model properties definition function.
*/
private getModelIdentifier(): string
{
return Reflect.getMetadata(modelIdentifierMetadataKey, this);
}
protected abstract SDefinition(): ModelDefinition<THIS>;
/**
* 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<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.
* @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<unknown, unknown>) => 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, 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.
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.
}
}

View File

@ -9,7 +9,7 @@ export type ConstructorOf<T> = { new(): T; }
/**
* 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.
@ -49,7 +49,7 @@ export class ModelType<M extends Model> extends Type<any, M>
* Type of a Sharkitek model value.
* @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);
}

View File

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

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.
*/
class Author extends Model
class Author extends Model<Author>
{
@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<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)
{
super();
@ -34,23 +47,29 @@ class Author extends Model
/**
* A test model.
*/
class Article extends Model
class Article extends Model<Article>
{
@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<Article>
{
return "id";
}
@Property(SDecimal)
evaluation: number = undefined;
protected SDefinition(): ModelDefinition<Article>
{
return {
id: SDefine(SNumeric),
title: SDefine(SString),
authors: SDefine(SArray(SModel(Author))),
text: SDefine(SString),
evaluation: SDefine(SDecimal),
};
}
}
it("deserialize", () => {

View File

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