The EntityBase class is a lightweight, immutable, schema-driven entity base for frontend applications. It standardizes how data is modeled, supports deep relationships (hasMany, belongsTo), provides validation, serialization for APIs, and guarantees consistency across objects.
- ✅ Immutable by default
- ✅ Auto-injected primary fields like
id,_token,_destroy - ✅
hasManyandbelongsTorelationship support - ✅ Auto-generated getters for all attributes
- ✅ API-friendly with
.toParams()and.toObject() - ✅ Deep validations and error handling
- ✅ Useful helper methods like
isNewEntity(),validate(), etc.
Defines the default values for each attribute.
static defaultAttributes = {
name: '',
age: 0,
active: true
};Defines plural child relationships. Values will be stored as Map<idOrToken, EntityInstance>.
static hasMany = {
posts: PostEntity,
comments: CommentEntity
};Defines singular parent relationships. Each value is stored as a single entity instance.
static belongsTo = {
company: CompanyEntity
};Define validation rules per field using validator functions.
static validates = {
name: [(val) => ({ isValid: !!val, message: 'is required' })],
age: [(val) => ({ isValid: val >= 18, message: 'must be at least 18' })]
};const user = new UserEntity({ name: 'Alice', age: 25 });Every attribute becomes an automatic getter:
user.name // → "Alice"
user.get('name') // → "Alice"Relationships too:
user.company // → CompanyEntity instance
user.posts // → Map of PostEntity instancesconst updated = user.set('name', 'Bob');
console.log(user.name); // Alice
console.log(updated.name); // Bob
console.log(user !== updated); // trueclass PersonEntity extends EntityBase {
static defaultAttributes = {
name: '',
age: 0
};
}class CompanyEntity extends EntityBase {
static defaultAttributes = { name: '' };
}
class CarEntity extends EntityBase {
static defaultAttributes = { model: '', price: 0 };
}
class UserEntity extends EntityBase {
static defaultAttributes = {
name: '',
company: null,
cars: []
};
static belongsTo = { company: CompanyEntity };
static hasMany = { cars: CarEntity };
}
const user = new UserEntity({
name: 'João',
company: { name: 'Google' },
cars: [{ model: 'Tesla', price: 80000 }, { model: 'Civic', price: 40000 }]
});
console.log(user.company.name); // Google
console.log(user.cars.size); // 2const civicID = Array.from(user.cars.keys())[1];
const civic = user.cars.get(civicID);
const updatedCivic = civic.set('price', 45000);
const newCars = new Map(user.cars);
newCars.set(civicID, updatedCivic);
const updatedUser = user.set('cars', newCars);
console.log(updatedUser.cars.get(civicID).price); // 45000class AccountEntity extends EntityBase {
static defaultAttributes = { email: '' };
static validates = {
email: [(val) => ({
isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
message: 'is invalid'
})]
};
}
const account = new AccountEntity({ email: 'bad-email' });
const validated = account.validate();
console.log(validated.errors.fullMessages()); // ["Email is invalid"]Returns a plain JS object, including relationships:
const userObj = user.toObject();Used to build data for API submission:
const payload = user.toParams();You can override .toParams() in your own subclass to customize payloads.
| Method | Description |
|---|---|
.get(attr) |
Gets any attribute |
.set(attr, value) |
Returns new instance with updated value |
.updateAttributes() |
Updates multiple fields at once |
.validate() |
Runs all validations |
.isValid() |
Returns true if no validation errors |
.toObject() |
Serializes the full object |
.toParams() |
Payload-ready serialization |
.isNewEntity() |
Returns true if no ID exists |
.isPersisted() |
Returns true if entity has an ID |
.idOrToken |
Returns id if present, otherwise _token |
- Use
defaultAttributesin combination with schema builders. - Always use
.set()or.updateAttributes()— do not mutate manually. - Access related entities via auto-getters:
user.company.name. - Use
Mapmethods like.get(),.set(),.entries()forhasMany.
class MessageEntity extends EntityBase {
static defaultAttributes = { text: '', read: false };
}
class ThreadEntity extends EntityBase {
static defaultAttributes = { title: '', messages: [] };
static hasMany = { messages: MessageEntity };
}
class InboxEntity extends EntityBase {
static defaultAttributes = { user_id: null, threads: [] };
static hasMany = { threads: ThreadEntity };
}
const inbox = new InboxEntity({
user_id: 7,
threads: [
{
title: 'Support',
messages: [{ text: 'Hello' }, { text: 'How can I help?' }]
}
]
});
const threads = inbox.threads;
const firstThread = Array.from(threads.values())[0];
const messages = firstThread.messages;
console.log(messages.size); // 2Feel free to extend or override methods like:
class CustomUserEntity extends UserEntity {
toParams() {
return {
name: this.name,
company_id: this.company?.id,
car_ids: Array.from(this.cars.values()).map(c => c.id)
};
}
}Use jest or vitest to ensure:
- Reference integrity of relationships
- Immutability
- Validation and serialization behavior
Already includes support for Map, instanceof, and safe identity checks.
The EntityBase system is ideal for complex frontend apps (like React or Vue) that need:
- Entity modeling
- Safe data handling
- Form state isolation
- Predictable immutable updates