Skip to content

Commit f1403b5

Browse files
committed
Merge branch 'fix-bug-issues' into feat/permission-update
2 parents a783d5f + ae92b20 commit f1403b5

1 file changed

Lines changed: 41 additions & 12 deletions

File tree

fastapi_admin_kit/audit/listener.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Audit listener — SQLAlchemy event listeners that write audit rows atomically.
22
3-
AuditLog rows are created inside ``before_flush`` and added to the session
4-
via ``session.add()``. SQLAlchemy re-runs ``before_flush`` until no new
5-
pending objects appear, so the audit rows ride along in the same flush pass.
3+
AuditLog rows are created inside ``before_flush`` (UPDATE/DELETE) and
4+
``after_flush_postexec`` (CREATE) and added to the session via ``session.add()``.
5+
CREATE uses ``after_flush_postexec`` so the auto-generated primary key is available.
66
No IO, no queries, no ``MissingGreenlet`` — just already-loaded attributes.
77
"""
88

99
from __future__ import annotations
1010

11+
import threading
1112
from typing import Any
1213

1314
from sqlalchemy import event
@@ -18,6 +19,9 @@
1819
from fastapi_admin_kit.audit.diff import serialize_value
1920
from fastapi_admin_kit.audit.models import AuditLog
2021

22+
_pending_creates: dict[int, list[tuple[Any, dict[str, Any], dict[str, Any]]]] = {}
23+
_pending_lock = threading.Lock()
24+
2125

2226
def is_registered_model(obj: Any, registry: Any) -> bool:
2327
"""Check if a model class is registered with the admin."""
@@ -107,7 +111,7 @@ def attach_audit_listener(
107111
session_factory: Any,
108112
registry: Any,
109113
) -> None:
110-
"""Set up SQLAlchemy ``before_flush`` listener for audit logging.
114+
"""Set up SQLAlchemy ``before_flush`` and ``after_flush_postexec`` listeners.
111115
112116
Args:
113117
session_factory: The session factory (sync or async).
@@ -116,23 +120,27 @@ def attach_audit_listener(
116120

117121
@event.listens_for(Session, "before_flush")
118122
def before_flush(session: Session, flush_context: Any, instances: Any) -> None:
119-
"""Create AuditLog rows for all tracked mutations.
123+
"""Capture new objects and create AuditLog rows for UPDATE/DELETE.
120124
121-
Runs inside the same flush pass — ``session.add()`` puts the
122-
AuditLog into the pending set and SQLAlchemy will re-run
123-
``before_flush`` until no new objects appear. No queries, no
124-
lazy-loads, only already-loaded attribute history.
125+
New objects are stored to create audit rows in ``after_flush_postexec``
126+
where their auto-generated primary keys are available.
125127
"""
126128
context = get_audit_context()
129+
session_id = id(session)
127130

128-
# ── INSERT ──────────────────────────────────────────────────
131+
# ── INSERT (capture for after_flush_postexec) ───────────────
132+
new_items = []
129133
for obj in list(session.new):
130134
if not is_registered_model(obj, registry):
131135
continue
132136
if obj.__tablename__ == AuditLog.__tablename__:
133137
continue
134-
row = _build_audit_row(obj, "CREATE", context)
135-
session.add(row)
138+
snap = _snapshot_current(obj)
139+
new_items.append((obj, snap, context))
140+
141+
if new_items:
142+
with _pending_lock:
143+
_pending_creates[session_id] = new_items
136144

137145
# ── UPDATE ──────────────────────────────────────────────────
138146
for obj in list(session.dirty):
@@ -157,3 +165,24 @@ def before_flush(session: Session, flush_context: Any, instances: Any) -> None:
157165
snap = _snapshot_current(obj)
158166
row = _build_audit_row(obj, "DELETE", context, snapshot_data=snap)
159167
session.add(row)
168+
169+
@event.listens_for(Session, "after_flush_postexec")
170+
def after_flush_postexec(session: Session, flush_context: Any) -> None:
171+
"""Create AuditLog rows for CREATE mutations after flush.
172+
173+
This runs after the flush completes, so auto-generated primary keys
174+
are available on the ORM objects.
175+
"""
176+
session_id = id(session)
177+
178+
with _pending_lock:
179+
pending = _pending_creates.pop(session_id, [])
180+
181+
if not pending:
182+
return
183+
184+
for obj, snap, context in pending:
185+
# Update snapshot with the now-available id
186+
snap["id"] = getattr(obj, "id", None)
187+
row = _build_audit_row(obj, "CREATE", context, snapshot_data=snap)
188+
session.add(row)

0 commit comments

Comments
 (0)