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 theTransformService
and give it two paramters:myArticle
: the instance of the article we want to transformJsonTransformerSymbol
: thesymbol
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.