ClassBuilder
The ClassBuilder component of EncolaJS Hydrator provides a powerful and flexible way to create and enhance JavaScript classes with typed properties, custom methods, and reusable functionality through mixins.
Overview
The ClassBuilder allows you to:
- Define properties with automatic type casting
- Add methods to classes
- Apply mixins for reusable functionality
- Create model and collection classes with a fluent API
Basic Usage
import { CastingManager, ClassBuilder, BaseModel } from '@encola/hydrator';
// Set up the required dependencies
const castingManager = new CastingManager();
const builder = new ClassBuilder(castingManager);
// Create a base class
class Person extends BaseModel {
constructor(data = {}) {
super(data);
}
}
// Enhance the class with the builder
const Person = builder
.withClass(Person)
.add('props', {
firstName: 'string',
lastName: 'string',
age: 'number',
birthdate: 'date'
})
.add('methods', {
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
})
.build();
// Use the enhanced class
const person = new Person({
firstName: 'John',
lastName: 'Doe',
age: '30', // Will be cast to number
birthdate: '1993-05-15' // Will be cast to Date
});
console.log(person.getFullName()); // "John Doe"
console.log(person.age); // 30 (as a number)
console.log(person.birthdate instanceof Date); // trueProperty Definitions
The ClassBuilder allows you to define properties with type information:
builder
.withClass(MyClass)
.add('props', {
// Simple string type
name: 'string',
// Decimal with precision
price: 'decimal:2',
// Array of numbers
scores: 'array:number',
// Advanced property with computed value
fullName: {
type: 'string',
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
const parts = value.split(' ');
this.firstName = parts[0] || '';
this.lastName = parts[1] || '';
}
}
})
.build();Important! The
propsfeature works in a specific way and it might not work with any class. It is best suited for classes that extendBaseModel.
Advanced Property Types
The add('props', {...}) method supports advanced property definitions:
builder
.withClass(Product)
.add('props', {
// Regular property
name: 'string',
// Property with custom getter/setter
price: {
type: 'decimal:2',
get() {
// Return price with currency symbol
return `$${this._data.price || 0}`;
},
set(value) {
// Strip currency symbol and convert to number
if (typeof value === 'string') {
value = value.replace(/[^0-9.]/g, '');
}
this._data.price = this._castingManager.cast(value, 'decimal:2');
}
},
// Computed property (readonly)
isOnSale: {
type: 'boolean',
get() {
return this.salePrice !== null && this.salePrice < this.price;
}
}
})
.build();Creating Model Classes
The newModelClass method creates a model class with properties, mixins, and methods:
const User = builder.newModelClass(
// Properties
{
name: 'string',
email: 'string',
age: 'number',
isActive: 'boolean'
},
// Mixins
{
timestamps: {},
softDelete: {}
},
// Methods
{
isAdult() {
return this.age >= 18;
},
getDisplayName() {
return this.name || this.email.split('@')[0];
}
}
);
const user = new User({
name: 'John Doe',
email: '[email protected]',
age: '25' // Will be cast to number
});
console.log(user.isAdult()); // true
console.log(user.created_at instanceof Date); // trueCreating Collection Classes
The newCollectionClass method creates a collection class for a specific model:
// First create a model class
const Product = builder.newModelClass({
name: 'string',
price: 'number',
category: 'string'
});
// Then create a collection class for that model
const ProductCollection = builder.newCollectionClass(
Product,
{}, // No mixins in this example
{
// Add custom methods to the collection
getTotalPrice() {
return this.sum('price');
},
filterByCategory(category) {
return this.filterBy('category', category);
}
}
);
// Use the collection
const products = new ProductCollection([
{ name: 'Laptop', price: 1200, category: 'electronics' },
{ name: 'Phone', price: 800, category: 'electronics' },
{ name: 'Desk', price: 350, category: 'furniture' }
]);
console.log(products.getTotalPrice()); // 2350
console.log(products.filterByCategory('electronics').length); // 2Model Creation vs. Class Enhancement
The ClassBuilder provides two approaches to creating enhanced classes:
- Model Creation: Create a new model class using
newModelClass()which extend theBaseModelclass 2Class Enhancement: Enhance any existing class usingwithClass().add().build()
The class enhancement approach gives you more control and works with any class, while the model creation approach is more concise but requires extending from BaseModel.
Best Practices
- Reuse the ClassBuilder: Create one ClassBuilder instance and reuse it for all your class needs
- Leverage mixins: Create reusable mixins for common functionality
- Use meaningful property types: Take advantage of type parameters (e.g., 'decimal:2', 'array:number')
- Combine with CastingManager: Register custom types in your CastingManager for use in property definitions
TypeScript and JSDoc Support
When working with ClassBuilder in TypeScript or using JSDoc annotations, you'll want to properly type your dynamically created classes. Since the properties are added at runtime, you might need to define interfaces to represent them:
interface User {
name: string;
email: string;
age: number;
created_at?: Date;
updated_at?: Date;
isAdult(): boolean;
getDisplayName(): string;
}
// Create the model class
const User = builder.newModelClass<User>(
{
name: 'string',
email: 'string',
age: 'number'
},
{
timestamps: {}
},
{
isAdult() {
return this.age >= 18;
},
getDisplayName() {
return this.name || this.email.split('@')[0];
}
}
);
// TypeScript will recognize this as having all properties and methods
const user = new User({
name: 'John Doe',
email: '[email protected]',
age: 25
});
// When using the CastingManager you might need to specify the type
const castUser = castingManager.cast<User>(data, 'user');If you are using JSDoc, you can also use @typedef to define types for your classes:
/**
* @typedef {Object} User
* @property {string} name - The user's full name
* @property {number} age - The user's age
* @property {string} email - The user's email address
* @property {Date} created_at - When the user was created
* @property {Date} updated_at - When the user was last updated
* @property {() => boolean} isAdult - Checks if the user is an adult
* @property {() => string} getDisplayName - Gets a display-friendly name
*/
/**
* User model with typed properties and methods
* @extends {BaseModel}
* @implements {User}
*/
const User = builder.newModelClass(
// Properties
{
name: 'string',
email: 'string',
age: 'number'
},
// Mixins
{
timestamps: {}
},
// Methods
{
/** @returns {boolean} */
isAdult() {
return this.age >= 18;
},
/** @returns {string} */
getDisplayName() {
return this.name || this.email.split('@')[0];
}
}
);
// When using the class:
/**
* @type {User & BaseModel}
*/
const user = new User({
name: 'John Doe',
email: '[email protected]',
age: 25
});
// When using the CastingManager you might need to specify the type
/**
* @type {User & BaseModel}
*/
const castUser = castingManager.cast(data, 'user');