Mixins¶
AMSDAL provides built-in mixins that add common functionality to your models. Mixins use Python's MRO (Method Resolution Order) to hook into the model lifecycle — no extra configuration needed.
TimestampMixin¶
Automatically tracks when records are created and updated.
from amsdal.models.mixins import TimestampMixin
class Article(TimestampMixin, Model):
title: str
body: str
Fields¶
| Field | Type | Description |
|---|---|---|
created_at |
datetime \| None |
Set on creation |
updated_at |
datetime \| None |
Set on every update |
Both fields are populated automatically via pre_create / pre_update hooks.
AuditMixin¶
Automatically tracks who performed each action, what action was performed, and what changed. Works alongside TimestampMixin (which tracks when).
from amsdal.models.mixins import TimestampMixin, AuditMixin
class Contract(TimestampMixin, AuditMixin, Model):
client_name: str
amount: float
status: str = 'draft'
Fields¶
| Field | Type | Description |
|---|---|---|
audit_created_by |
str \| None |
Identity of the user who created the record |
audit_updated_by |
str \| None |
Identity of the user who last modified the record |
audit_action |
str \| None |
Last action: "create", "update", or "delete" |
audit_changes |
dict \| None |
Diff of changed fields: {"field": {"old": ..., "new": ...}} |
Actor Resolution¶
The current user is resolved automatically from the request context. When a request is handled by the AMSDAL server, the RequestContextMiddleware stores the request in a context variable. AuditMixin reads request.user.identity from that context.
If no authenticated user is available (e.g., in management commands or scripts), audit fields are set to None.
How Changes Are Tracked¶
On create, audit_action is set to "create" and audit_created_by is set to the current user. No diff is computed since all fields are new.
On update, the mixin refetches the previous version from the database, compares field values, and stores the diff:
contract.status = 'signed'
contract.save()
# contract.audit_changes == {'status': {'old': 'draft', 'new': 'signed'}}
# contract.audit_updated_by == 'user@example.com'
# contract.audit_action == 'update'
On delete, audit_action is set to "delete" and audit_updated_by is set to the current user.
Audit Trail with Lakehouse¶
Since AMSDAL uses a dual-database architecture (state DB + lakehouse), every version stored in the lakehouse preserves the audit fields. This gives you a complete audit trail without needing a separate audit log model — just query the lakehouse history.
Auto-Managed Fields¶
Both TimestampMixin and AuditMixin declare __auto_managed_fields__ — a set of field names that are populated automatically by the mixin. These fields are:
- Excluded from frontend form controls (via
get_auto_managed_fields()) - Excluded from the changes diff computation
You can use this mechanism in your own mixins:
class SoftDeleteMixin:
__auto_managed_fields__: ClassVar[set[str]] = {'is_deleted', 'deleted_at'}
is_deleted: bool = False
deleted_at: datetime | None = None
Bulk Operations¶
Bulk operations (bulk_create, bulk_update, bulk_delete) skip model hooks. Use the stamp_timestamp() and stamp_audit() methods to populate mixin fields before bulk calls:
for obj in objects:
obj.stamp_timestamp(action='create')
obj.stamp_audit(action='create')
MyModel.objects.bulk_create(objects)
for obj in objects:
obj.stamp_timestamp(action='update')
obj.stamp_audit(action='update', changes={'status': {'old': 'draft', 'new': 'published'}})
MyModel.objects.bulk_update(objects)
for obj in objects:
obj.stamp_audit(action='delete')
MyModel.objects.bulk_delete(objects)
Combining Mixins¶
You can use both mixins together. Place them before Model in the class definition:
class Invoice(TimestampMixin, AuditMixin, Model):
number: str
total: float
The combined auto-managed fields are: created_at, updated_at, audit_created_by, audit_updated_by, audit_action, audit_changes — all excluded from forms and diffs automatically.