Model

A model is a class that can represent any structured data. A good usage is to represent database entities on the client side for example. Models can have relations with other models and be transformed to other formats (more on that later).

Functionalities related to models are divided in several packages to avoid unused dependencies:

  • @banquette/model (discussed here): that's the base package all other packages depend on. It contains the core logic and services.
  • @banquette/model-form (doc): tools to transform your models into forms and to keep them in sync.
  • @banquette/model-validation (doc): to add validation logic to your models.

A basic model

A model is nothing more than a class on which you add decorators to define metadata.

Imagine we want to represent an article in our application. You can create an Article class that will be our model:

export class Article {
    public id: number;
    public title: string;
    public content: string;
    public tags: string[];
    public creationDate: Date;
}

But this class doesn't do much as is. So we can decorate it to make it smarter. If you're not familiar with decorators, you should take a look at the Metadata Reflection APIopen in new window first.

There are basically two types of actions you can do on a model with the built-in tools:

  • You can transform it into something else (a string, a form, or anything else you want),
  • You can validate it to ensure all its attributes match a set of requirements.

We will see how all of this works in the next chapters, and finally see how you can extend this to add your own behaviors.

Basic decorators

First let's take a look at the very basic decorators that apply to all models, no matter you want to do. Specialized decorators we'll see next may use the metadata defined by these decorators.

@Relation

Property

Define that a property is an instance of another model.

class Category {
    public name: string;
}

class User {
    @Relation(Category)
    public category: Category;
}

It works the same if the property is an array of models, of any depth:

class User {
    @Relation(Tag)
    public tags: Tag[];
}

NOTE

Here we only declare that the value of the property contain a certain type of model. There is no type of relation or anything like that.

The fact that the property contains an array of models or any other data structure has no importance here.

The @Relation in itself does nothing, it only stores the information that will be used by other part of the system to know what they're dealing with.


@Alias

Class

Define an alternative way of referring to the model. Just like @Relation, it only defines an information that is then used by other decorators or services.

const CategorySymbol = Symbol();

@Alias('Category')
@Alias('AnotherAlias') // You can define as many aliases as you want
@Alias(CategorySymbol) // An alias can be a `string` or a `symbol`
class Category {
    
}

class User {
    @Relation('Category')
    public category: Category;
}

TIP

@Alias is optional. The model can always be referred to by its constructor. But adding a string alias for example, is especially useful to handle circular dependencies:

a.ts:

import { Relation } from '@banquette/model';
import { B } from './b';

@Alias('A')
export class A {
    @Relation('B')
    public b: B;
}

b.ts:

import { Relation } from '@banquette/model';
import { A } from './a';

@Alias('B')
export class B {
    // If we tried to reference A by its constructor, we would have an error here, 
    // because the A import is undefined when the decorator is executed
    // if A is imported first. 
    // The opposite would be true if B is imported first.
    @Relation('A')
    public a: A;
}

@Factory

Class

Define a function that will be used to create a new instance of the model.

import { Factory } from '@banquette/model';

@Factory(() => new User('guest'))
class User {
    public constructor(type: string) {}
}

TIP

This can be really useful if your model uses dependency injection:

import { Injector } from '@banquette/dependency-injection';
import { Factory } from '@banquette/model';

@Factory(() => Injector.Get(User))
class User {
    public constructor(@Inject(MyService) private myService) {}
}

Because most of the time you'll not create your model instances yourself. We'll see why in the next chapters.

Metadata service

The decorators discussed here store their information in a service called ModelMetadataService that you can access through the Injector:

import { Injector } from '@banquette/dependency-injection';
import { ModelMetadataService } from '@banquette/model';

Injector.Get(ModelMetadataService);

You can feed the service manually but the recommended way is to decorate your model.