Model Mixins
Hysteria ORM provides simple mixins that let you easily add common functionality to your models. Mixins are pre-built model classes that you can extend to inherit useful columns, behaviors, and patterns without writing repetitive code.
What are Mixins?
Mixins are reusable model classes that contain common sets of columns and behaviors. Instead of manually adding the same columns (like createdAt
, updatedAt
, id
) to every model, you can extend a mixin to get all that functionality automatically.
Benefits:
- DRY (Don't Repeat Yourself): Avoid duplicating common column definitions across models
- Consistency: Ensure all models follow the same patterns for timestamps, IDs, etc.
- Maintainability: Update common functionality in one place
- Type Safety: Full TypeScript support with proper typing
Available Mixins
TimestampedModel
Adds automatic timestamp tracking with createdAt
, updatedAt
, and deletedAt
columns.
import { TimestampedModel, column } from 'hysteria-orm';
export class User extends TimestampedModel {
@column()
declare name: string;
@column()
declare email: string;
// createdAt, updatedAt, and deletedAt are inherited from TimestampedModel
}
Inherited Columns:
createdAt: Date
- Automatically set when record is created (autoCreate: true
)updatedAt: Date
- Automatically updated when record is modified (autoCreate: true, autoUpdate: true
)deletedAt: Date | null
- Used for soft delete functionality
UuidModel
Provides a UUID primary key column that's automatically generated.
import { UuidModel, column } from 'hysteria-orm';
export class Product extends UuidModel {
@column()
declare name: string;
@column.float()
declare price: number;
// id: string (UUID) is inherited from UuidModel
}
Inherited Columns:
id: string
- UUID primary key with automatic generation
AutogeneratedModel
Provides an auto-incrementing integer primary key.
import { AutogeneratedModel, column } from 'hysteria-orm';
export class Category extends AutogeneratedModel {
@column()
declare name: string;
@column()
declare description: string;
// id: BigInt (auto-incrementing) is inherited from AutogeneratedModel
}
Inherited Columns:
id: BigInt
- Auto-incrementing integer primary key
User (Base User Model)
A comprehensive user model with common authentication fields.
import { User as BaseUser, column } from 'hysteria-orm';
export class User extends BaseUser {
@column()
declare name: string;
@column({ hidden: true })
declare password: string;
// id, email, createdAt, updatedAt, deletedAt are inherited from BaseUser
}
Inherited Columns:
id: BigInt
- Auto-incrementing primary keyemail: string
- User email addresscreatedAt: Date
- Account creation timestampupdatedAt: Date
- Last modification timestampdeletedAt: Date | null
- Soft delete timestamp
Combining Mixins
You can create your own mixins by combining existing ones or extending them with additional functionality:
import { TimestampedModel, column } from 'hysteria-orm';
// Create a custom mixin that extends TimestampedModel
export class AuditableModel extends TimestampedModel {
@column({ hidden: true })
declare createdBy: string | null;
@column({ hidden: true })
declare updatedBy: string | null;
@column({ hidden: true })
declare deletedBy: string | null;
}
// Use your custom mixin
export class Order extends AuditableModel {
@column.uuid({ primaryKey: true })
declare id: string;
@column.float()
declare total: number;
@column()
declare status: string;
// Inherits: createdAt, updatedAt, deletedAt, createdBy, updatedBy, deletedBy
}
Creating Custom Mixins
You can create your own mixins for domain-specific patterns:
import { Model, column } from 'hysteria-orm';
// Mixin for soft-deletable models
export class SoftDeleteModel extends Model {
@column.date()
declare deletedAt: Date | null;
static softDeleteColumn = "deletedAt";
static softDeleteValue = new Date().toISOString();
}
// Mixin for versioned models (optimistic locking)
export class VersionedModel extends Model {
@column.integer({ default: 1 })
declare version: number;
static async beforeUpdate(data: any): Promise<void> {
data.version = (data.version || 1) + 1;
}
}
// Mixin for models with slug support
export class SlugModel extends Model {
@column()
declare slug: string;
static async beforeInsert(data: any): Promise<void> {
if (!data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
}
}
Usage Examples
Simple Blog Post with Timestamps
import { TimestampedModel, column, belongsTo } from 'hysteria-orm';
import { User } from './User';
export class Post extends TimestampedModel {
@column.uuid({ primaryKey: true })
declare id: string;
@column()
declare title: string;
@column()
declare content: string;
@column()
declare userId: string;
@belongsTo(() => User, 'userId')
declare user: User;
// createdAt, updatedAt, deletedAt inherited from TimestampedModel
}
E-commerce Product with UUID
import { UuidModel, column, hasMany } from 'hysteria-orm';
import { OrderItem } from './OrderItem';
export class Product extends UuidModel {
@column()
declare name: string;
@column()
declare description: string;
@column.float()
declare price: number;
@column.integer()
declare stock: number;
@column.date({ autoCreate: true })
declare createdAt: Date;
@hasMany(() => OrderItem, 'productId')
declare orderItems: OrderItem[];
// id: string (UUID) inherited from UuidModel
}
User Management System
import { User as BaseUser, column, hasMany } from 'hysteria-orm';
import { Post } from './Post';
export class User extends BaseUser {
@column()
declare name: string;
@column({ hidden: true })
declare password: string;
@column()
declare role: 'admin' | 'user' | 'moderator';
@column.boolean()
declare isActive: boolean;
@hasMany(() => Post, 'userId')
declare posts: Post[];
// id, email, createdAt, updatedAt, deletedAt inherited from BaseUser
}
Best Practices
1. Choose the Right Mixin
Select mixins based on your model's primary key and functionality needs:
- Use
UuidModel
for distributed systems or when you need globally unique IDs - Use
AutogeneratedModel
for traditional auto-incrementing integer IDs - Use
TimestampedModel
when you only need timestamps (no primary key) - Use
User
mixin for authentication-related models
2. Combine Wisely
When creating custom mixins, think about composition:
// Good: Specific, focused mixin
export class TimestampedUuidModel extends UuidModel {
@column.date({ autoCreate: true })
declare createdAt: Date;
@column.date({ autoCreate: true, autoUpdate: true })
declare updatedAt: Date;
}
// Better: Use existing mixins
export class MyModel extends TimestampedModel {
@column.uuid({ primaryKey: true })
declare id: string;
// Override the inherited deletedAt if you don't need soft deletes
}
3. Override When Necessary
You can override inherited columns if needed:
export class SpecialUser extends User {
// Override the inherited id to use UUID instead of BigInt
@column.uuid({ primaryKey: true })
declare id: string;
// Override email to add validation
@column({
type: "varchar",
length: 320, // RFC 5321 max email length
openApi: { type: "string", format: "email", required: true },
})
declare email: string;
}
4. Document Your Custom Mixins
When creating reusable mixins, document them well:
/**
* @description Mixin for multi-tenant applications
* Adds tenantId column and automatic tenant filtering
*/
export class TenantModel extends Model {
@column()
declare tenantId: string;
static beforeFetch(queryBuilder: ModelQueryBuilder<any>): void {
// Add tenant filtering logic here
// queryBuilder.where('tenantId', getCurrentTenantId());
}
}
OpenAPI Integration
All built-in mixins include proper OpenAPI schemas for automatic API documentation generation:
// The mixins automatically include OpenAPI metadata
export class Product extends UuidModel {
@column({
openApi: { type: "string", maxLength: 100, required: true }
})
declare name: string;
// id already has OpenAPI metadata from UuidModel
}
Migration Considerations
When using mixins, ensure your database migrations include the inherited columns:
// In your migration file
export async function up(queryBuilder: QueryBuilder): Promise<void> {
await queryBuilder.schema.createTable('products', (table) => {
table.uuid('id').primary(); // From UuidModel
table.string('name').notNullable();
table.float('price').notNullable();
table.timestamp('created_at').defaultTo(queryBuilder.fn.now()); // From TimestampedModel
table.timestamp('updated_at').defaultTo(queryBuilder.fn.now()); // From TimestampedModel
table.timestamp('deleted_at').nullable(); // From TimestampedModel
});
}
See Also
- Model Basics - Learn about basic model definition and decorators
- Model Hooks - Understand lifecycle hooks and custom behaviors
- Model Views - Working with database views
- Standard Methods - Learn about CRUD operations on models
Next: Model Hooks & Lifecycle