Bases: DatabaseAdapter
Snapshots the pre-write entity state into <kind>_revision.
revision_fields (skel class var) gates which changes create a revision;
["*"] (the default) reacts to any change. Each snapshot copies the old
entity verbatim plus origin_key, revision_index (per-origin counter),
revision_date and revision_change_list.
Source code in src/viur/revision/adapter.py
| class RevisionDatabaseAdapter(DatabaseAdapter):
"""Snapshots the pre-write entity state into ``<kind>_revision``.
``revision_fields`` (skel class var) gates which changes create a revision;
``["*"]`` (the default) reacts to any change. Each snapshot copies the old
entity verbatim plus ``origin_key``, ``revision_index`` (per-origin counter),
``revision_date`` and ``revision_change_list``.
"""
def prewrite(self, skel: SkeletonInstance, is_add: bool, change_list: t.Iterable[str] = ()):
if is_add:
return
revision_fields: list[str] = getattr(skel.skeletonCls, "revision_fields", ["*"])
change_list = list(change_list)
if "*" in revision_fields:
should_create = bool(change_list)
else:
should_create = bool(set(change_list) & set(revision_fields))
if not should_create:
return
origin_key = skel.dbEntity.key
# prewrite runs before db.Put(), so this is still the old state.
old_entity = db.Get(origin_key)
if old_entity is None:
logger.warning(
"RevisionDatabaseAdapter: no existing entity found for %s – skipping revision",
origin_key,
)
return
revision_kind = f"{skel.kindName}_revision"
existing = (
db.Query(revision_kind)
.filter("origin_key =", origin_key)
.order(("revision_index", db.SortOrder.Descending))
.getEntry()
)
# Within the window the latest revision already holds the "before" state;
# just merge the change_list into it (collapses auto-save spam).
coalesce_window = getattr(
skel.skeletonCls, "revision_coalesce_window", _DEFAULT_COALESCE_WINDOW
)
if coalesce_window and existing and existing.get("revision_date"):
age = utils.utcNow() - existing["revision_date"]
if age < coalesce_window:
merged = list(dict.fromkeys(
list(existing.get("revision_change_list") or []) + change_list
))
existing["revision_change_list"] = merged
db.Put(existing)
logger.debug(
"RevisionDatabaseAdapter: coalesced change into revision %s for %s (age %s)",
existing.get("revision_index"),
origin_key,
age,
)
return
revision_index = (existing["revision_index"] + 1) if existing else 1
new_key = db.AllocateIDs(db.Key(revision_kind))
revision_entity = db.Entity(new_key)
revision_entity.exclude_from_indexes = set(old_entity.exclude_from_indexes)
for k, v in old_entity.items():
revision_entity[k] = v
revision_entity["origin_key"] = origin_key
revision_entity["revision_index"] = revision_index
revision_entity["revision_date"] = utils.utcNow()
revision_entity["revision_change_list"] = change_list
db.Put(revision_entity)
logger.debug(
"RevisionDatabaseAdapter: revision %d created for %s (old state preserved)",
revision_index,
origin_key,
)
|