We have done a good job in our resources so far, let's go one more step, and define a way to customize our resource output using boot
and extend
methods.
The need
Why would we need these methods? Well, sometimes we need to customize the output of our resource, for example, we may need to add a new key to the output, or we may need to remove a key from the output, or we may need to change the value of a key in the output.
These changes are mostly conditional, which means, for example, if current resource is User Resource
and the current user is the same as the one in the resource, then we may need to add a new key to the output, for example, let's say we want to add a new key called cart
to the output, but we only want to add this key if the current user is the same as the user in the resource.
Another use case, if you're working with courses api, and you're returning list of courses, or just a single course, we want to add a new key to the final output called isSubscribed
which is a boolean value, and we want to set this value to true
if the current user is subscribed to the course, otherwise the key will be set to false
or may be not returned at all, so that's a must
feature to be implemented in our resources.
Boot And Extend
Basically, these are two methods, the first one boot
will be called before transforming our output
property to the final output, and the second one extend
will be called after transforming our output
property to the final output.
So you can use boot
to add new keys to the output, replace or remove any keys from it and you can use extend
to add new keys to the output, replace or remove any keys from it but after the output has been transformed.
Implementation
Implementation is so simple, these are two methods (non-static of course
), the first one will be called before transforming the output, and the second one will be called after transforming the output.
Please note that we'll make these two methods async methods so you can perform any async operation inside them like fetching data from the database.
// core/resources/resource.ts
// ...
export default class Resource {
/**
* Boot method
* Called before transforming the resource
*/
public async boot() {
//
}
/**
* Extend the resource output
* Called after transforming the resource
*/
public async extend() {
//
}
// ...
}
Simple two async empty methods, now we are going to update our toJSON
function to be async
method and call these two methods before and after transforming the output.
// core/resources/resource.ts
// ...
export default class Resource {
// ...
/**
* Transform resource to object, that's going to be used as the final output
*/
public async toJSON() {
await this.boot();
await this.transformOutput();
await this.extend();
return this.data;
}
// ...
}
You might be wondered, where is that giant code block that was in our toJSON
method, well i just moved it to transformOutput
method, that's much cleaner now.
We can also make any callable function that will be used in the transforming process to be async
as well.
Async transformation
Now let's update our transformOutput
method to be async
method as well.
Inside it, there will be a call to transformValue
this method must be awaited
so we can make it async
as well.
// core/resources/resource.ts
// ...
export default class Resource {
// ...
/**
* Transform final output
*/
protected async transformOutput() {
for (const key in this.output) {
// first check if key is disabled
if (this.isDisabledKey(key)) continue;
if (!this.isAllowedKey(key)) continue;
// get value type
const valueType = this.output[key];
// now get the value from the given resource data
const value = get(
this.resource,
key,
get(this.defaults, key, missingKey),
);
if (value === missingKey) {
continue;
}
if (Is.empty(value)) continue;
if (Array.isArray(value)) {
this.data[key] = value.map(
async item => await this.transformValue(item, valueType),
);
} else {
this.data[key] = await this.transformValue(value, valueType);
}
}
}
// ...
}
I added async
keyword to the transformOutput
method, and i added await
keyword to the transformValue
method call.
Note that we have to make the callback in the map function to be
async
as well, so we canawait
thetransformValue
method call.
Now let's update our transformValue
method to be async
method as well.
// core/resources/resource.ts
// ...
export default class Resource {
// ...
/**
* Transform final output
*/
protected async transformOutput() {
for (const key in this.output) {
// first check if key is disabled
if (this.isDisabledKey(key)) continue;
if (!this.isAllowedKey(key)) continue;
// get value type
const valueType = this.output[key];
// now get the value from the given resource data
const value = get(
this.resource,
key,
get(this.defaults, key, missingKey),
);
if (value === missingKey) {
continue;
}
if (Is.empty(value)) continue;
if (Array.isArray(value)) {
this.data[key] = value.map(
async item => await this.transformValue(item, valueType),
);
} else {
this.data[key] = await this.transformValue(value, valueType);
}
}
}
/**
* Transform value
*/
protected async transformValue(value: any, valueType: any) {
if (typeof valueType === "string") {
value = this.cast(value, valueType);
} else if (valueType.prototype instanceof Resource) {
if (!this.isValidResourceValue(value)) return null;
value = new valueType(value);
} else if (typeof valueType === "function") {
value = await valueType.call(this, value);
}
return value;
}
// ...
}
I added async
keyword to the transformValue
method, and i added await
keyword to the valueType
function call.
Now let's give it a try in our UserResource
class.
// src/app/users/resources/user-resource.ts
import Resource from "core/resources/resource";
export default class UserResource extends Resource {
/**
* {@inheritDoc}
*/
public async boot(): Promise<void> {
this.data.welcome = true;
}
/**
* {@inheritDoc}
*/
public async extend(): Promise<void> {
this.data.goodbye = true;
}
}
Now you should see something like this:
{
"welcome": true,
"isActive": true,
"isPhoneVerified": false,
"name": "John Doe",
"email": "hassanzohdy@gmail.com",
"image": "http://127.0.0.1:3000/uploads/users/my-image.jpg",
"goodbye": true
},
But this really is not going to work, why? because our method toJSON
is basically sync
method and now we changed it to be async
method, thus this won't work anymore.
And that's going to be our next topic.
🎨 Conclusion
In this chapter, we talked about the importance of booting
and extending
resources and why it should be in our codebase.
In our next chapter, we're going to see how to make our toJSON
method to be async
method.
That's where our powered Response class comes in.
☕♨️ Buy me a Coffee ♨️☕
If you enjoy my articles and see it useful to you, you may buy me a coffee, it will help me to keep going and keep creating more content.
🚀 Project Repository
You can find the latest updates of this project on Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
🎞️ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
📚 Bonus Content 📚
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Top comments (0)