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:
parent
13072b453f
commit
1c3c87a4a6
8 changed files with 215 additions and 187 deletions
118
README.md
118
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<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),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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
34
src/Model/Definition.ts
Normal 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);
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
"outDir": "./lib/",
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
|
|
Loading…
Reference in a new issue