Skip to main content

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 key
  • email: string - User email address
  • createdAt: Date - Account creation timestamp
  • updatedAt: Date - Last modification timestamp
  • deletedAt: 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


Next: Model Hooks & Lifecycle