Skip to content

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.