Transform your model

One of the most important advantage of having a model is the ability to transform it into something else. Here we'll continue with our Article class from the previous chapter and see how we can transform it into a JSON string.

At first glance you may think, why not just do:

JSON.stringify(myArticleInstance);

It may work for basic cases, but what if you want to control the format of the date? Or if you don't want certain attributes in the JSON output?

Introducing @Json

You can put a @Json() decorator on the attributes you want to be convertible to a JSON string. For example let's say we don't want the id in the JSON and we want a datetime format for the date:

import { Json, DateToString } from '@banquette/model';

export class Article {
    public id: number;

    @Json()
    public title: string;

    @Json()
    public content: string;

    @Json()
    public tags: string[];

    @Json(DateToString('YYYY-mm-dd'))
    public creationDate: Date;
}

Here the @Json decorator only register metadata that will then be used when we want to transform the model into a JSON string.

Here we register 4 attributes for the model Article: title, content, tags and creationDate.

NOTE

Because the attribute id doesn't have a @Json decorator, it is invisible for the Json transformer.

TransformService

Now that we have some metadata for our model, we can use them do to something.

In our current example, we can use the TransformService to transform the model into a JSON string:

import { Injector } from '@banquette/dependency-injection';
import { TransformService, JsonTransformerSymbol } from '@banquette/model';

// We create an Article instance and whatever values we want to it.
const myArticle = new Article();
myArticle.title = 'Hello World!';
myArticle.content = 'That\'s a first!';
myArticle.tags = ['test', 'dev'];
myArticle.creationDate = new Date();

// We get the TransformService.
const transformService = Injector.Get(TransformService);

// We convert it to JSON
const transformResult = transformService.transform(myArticle, JsonTransformerSymbol);

// transformResult.result:
// {"title": "Hello World!", "content": "That's a first!", "tags": ["test", "dev"], "creationDate": "2021-11-05"}














 



So to take it line by line:

  • line 1-2: We import what we need,
  • lines 5-9: We create an article with test data,
  • line 12: We inject the TransformService from @banquette/model. That's the service responsible for converting a model into something else,
  • line 15: We call the method transform on the TransformService and give it two paramters:
    • myArticle: the instance of the article we want to transform
    • JsonTransformerSymbol: the symbol of the transformer we want to use. Here we want a JSON string so we use the symbol of the corresponding transformer.

We then receive a TransformResult object, that contains a result attribute with our JSON string.

This can seem pretty complicated to just replace JSON.stringify(), but bear with me, it will get powerful.

TransformResult

As you can see in the previous example, the transform method returns a TransformResult object. This object contains the following attributes:

Attribute Description
result The result of the transformation.
ready true if the result is ready.
error true if the transformation failed (e.g. an exception has been thrown)
waiting true if the transformation is still running (only if async).
status Centralize the 3 flags seen above in a single value (enum).
promise A promise that will resolve when the transformation is done (if async).
parent Parent result.

TIP

If you've read the documentation in order, you can see that the way transformers return their result is similar to what the validators do. This is by design. You will see this pattern several times through the docs.

Just like validators, the goal here is to allow transformers to do asynchronous processing if they need to, but without making everything asynchronous.

The TransformResult is always returned synchronously, that's the responsibility of the user to then check if the result is ready or not.

Also, having a result object immediately allow you to share it with your view immediately. You can even make the view react to changes in the TransformResult object itself, making the state management a breeze.

Root transformers

In our previous example we've used the symbol "JsonTransformerSymbol".

This symbol is simply an identifier, used by the TransformService to select the correct transformer to use.

Each transformer has a symbol (its identifier) and one or multiple decorators (to configure its behavior). There are a few built-in transformers that comes with Banquette:

Name Identifier Package Description Link
Json JsonTransformerSymbol @banquette/model Converts a model into a JSON string. Doc
Pojo PojoTransformerSymbol @banquette/model Converts a model into an object literal. Doc
Form FormTransformerSymbol @banquette/model-form Converts a model into a FormObject. Doc

NOTE

These are in fact a special type of transformer, called root transformers. Root because they only apply on a model class as a whole, not on individual properties.

This type of transformer is always called through the TransformService, that will select the right one based on the symbol you give.

Property transformers

Property transformers are exactly the same principle as root transformers (they transform the value they get as input into something else), but are made to apply on properties only.

They are exposed through a factory function accessible globally, for example:

class User {
    @Json(Primitive())
    public username: string;
}

Here Primitive is a transformer factory. Calling the function will return the actual transformer.

This allows you to configure the transformer by giving it options. For example the Primitive transformer can take a type as argument:

class User {
    @Json(Primitive(Type.String))
    public username: number = 2;
}

In this example, this will ensure the value of username is always a string when converted to Json.

TIP

As you can see, the DateToString function we used in our first example is a property transformer:

export class Article {
    [...]

    @Json(DateToString('YYYY-mm-dd'))
    public creationDate: Date;
}

All built-in transformers (root and property) are described in detail in the next chapter.

WARNING

If you want a property to be assignable via the constructor, you may be tempted to do this:

public constructor(@Json(Primitive()) public username: string) {
}

instead of:

@Json(Primitive())
public username: string;

public constructor(username: string) {
    this.username = username;
}

But please DON'T! It will work in development, but may break in production.

That's because the first notation is just a shorthand for the second.

This means that, when transpiled, there is actually two variables username, one local (the constructor parameter), and one class attribute.

And the decorator will apply on the local variable.

That can be a major issue if the transpiler also rename the local variable to make it shorter, in which case the name visible to the decorator will not match the class property name anymore. This will break the logic behind many of these decorators.

Always define your decorators on class properties, or ensure that your transpiler doesn't mangle the names of local variables (which could have a huge impact on your bundle size).

Sync vs Async

Any transformer can be asynchronous if it needs to, but it's optional.

In either case, you'll always receive a TransformResult synchronously, no matter what happens inside the transformer.

If you're 100% certain the transformer you're calling is synchronous, you can use the result immediately and totally ignore the promise part.

If you have a doubt, you can use the onReady() method that will return a promise that will resolve when the result is ready, even if the result is in fact synchronous:

transformService.transform(myArticle, JsonTransformerSymbol).onReady((result: TransformResult) => {
    // You're guaranteed the result is ready here (except if the transform failed).
});

TIP

You can also check the flags or the promise attribute. If the result is synchronous the promise will be null.


Transform inverse

Since the beginning we've only talked about how to transform a model into something else, but transformers can also do the opposite.

To be more precise, all transformers can implement two methods:

  • transform: to transform a model instance into the target format,
  • transformInverse: to transform the target format back into a model instance.

NOTE

Both of them are optional, so you can have a different transformer doing the conversation only in one way or the other. Most transformers will do both though.

Using the TransformService, an inverse transform is done like this:

const result = transformService.transformInverse(
    json,   // The data to transform
    Article // The type of the model
);

TIP

Here the type of the model can be the constructor or an alias:

@Alias('ArticleEntity')
class Article {
    [...]
}

const result = transformService.transformInverse(
    '{"title": "Test article"}',    // The data to transform
    'ArticleEntity'                 // Here we use the alias instead
);

Transform transversal

As a general rule of thumb, the "default" format is the model class:

  • When you transform, you transform a model into something else,
  • And when you transformInverse, you transform that "something else" back into a model class.

But now imagine you want to transform a JSON string into a form.

You could do a JsonToForm transformer but it's silly. If you want to be able to switch between any format you'll quickly be overwhelmed by the number of transformers.

So the solution is to use the model class as a transition format.

Meaning to convert a JSON string into a form you do two transforms:

  • First you convert the JSON string back into a model (transformInverse),
  • Then you convert the model into a form (transform).

That's exactly what the method transformTransversal does:

const result = transformService.transformTransversal(
    json,                   // The data to transform
    JsonTransformerSymbol,  // The type of transformer to use for the "inverse" transform
    Article,                // The type of the model
    FormTransformerSymbol   // The type of transformer to use for the final transform
);

NOTE

The value of transformTransversal is that it keeps all the "asynchronousity" concerns out of your back and returns a single TransformResult that contains the final result of the sequence of transforms.