forked from retoor/devplacepy
chore: document project-wide soft delete pattern and add deleted_by column to all tables
This commit is contained in:
@@ -52,6 +52,8 @@ def create_instance(row: dict) -> dict:
|
||||
"stopped_at": "",
|
||||
"created_at": now(),
|
||||
"updated_at": now(),
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
base.update(row)
|
||||
base["uid"] = uid
|
||||
@@ -63,13 +65,15 @@ def get_instance(uid: str):
|
||||
table = get_table("instances")
|
||||
if not _exists("instances"):
|
||||
return None
|
||||
return table.find_one(uid=uid) or table.find_one(slug=uid)
|
||||
return table.find_one(uid=uid, deleted_at=None) or table.find_one(
|
||||
slug=uid, deleted_at=None
|
||||
)
|
||||
|
||||
|
||||
def list_instances(project_uid: str = None) -> list:
|
||||
if not _exists("instances"):
|
||||
return []
|
||||
filters = {}
|
||||
filters = {"deleted_at": None}
|
||||
if project_uid:
|
||||
filters["project_uid"] = project_uid
|
||||
return sorted(
|
||||
@@ -82,13 +86,13 @@ def list_instances(project_uid: str = None) -> list:
|
||||
def all_instances() -> list:
|
||||
if not _exists("instances"):
|
||||
return []
|
||||
return list(get_table("instances").find())
|
||||
return list(get_table("instances").find(deleted_at=None))
|
||||
|
||||
|
||||
def find_instance_by_ingress(slug: str):
|
||||
if not slug or not _exists("instances"):
|
||||
return None
|
||||
matches = list(get_table("instances").find(ingress_slug=slug))
|
||||
matches = list(get_table("instances").find(ingress_slug=slug, deleted_at=None))
|
||||
for instance in matches:
|
||||
if instance.get("status") == ST_RUNNING:
|
||||
return instance
|
||||
@@ -99,8 +103,10 @@ def update_instance(uid: str, changes: dict) -> None:
|
||||
get_table("instances").update({"uid": uid, "updated_at": now(), **changes}, ["uid"])
|
||||
|
||||
|
||||
def delete_instance(uid: str) -> None:
|
||||
get_table("instances").delete(uid=uid)
|
||||
def delete_instance(uid: str, deleted_by: str = "system") -> None:
|
||||
get_table("instances").update(
|
||||
{"uid": uid, "deleted_at": now(), "deleted_by": deleted_by}, ["uid"]
|
||||
)
|
||||
|
||||
|
||||
# ---------------- events / metrics / schedules ----------------
|
||||
@@ -183,6 +189,8 @@ def create_schedule(
|
||||
"run_count": 0,
|
||||
"created_at": now(),
|
||||
"updated_at": now(),
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
)
|
||||
return get_table("instance_schedules").find_one(uid=uid)
|
||||
@@ -191,7 +199,11 @@ def create_schedule(
|
||||
def list_schedules(instance_uid: str) -> list:
|
||||
if not _exists("instance_schedules"):
|
||||
return []
|
||||
return list(get_table("instance_schedules").find(instance_uid=instance_uid))
|
||||
return list(
|
||||
get_table("instance_schedules").find(
|
||||
instance_uid=instance_uid, deleted_at=None
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def due_schedules(now_iso: str) -> list:
|
||||
@@ -199,7 +211,7 @@ def due_schedules(now_iso: str) -> list:
|
||||
return []
|
||||
return [
|
||||
r
|
||||
for r in get_table("instance_schedules").find(enabled=1)
|
||||
for r in get_table("instance_schedules").find(enabled=1, deleted_at=None)
|
||||
if r.get("next_run_at") and r["next_run_at"] <= now_iso
|
||||
]
|
||||
|
||||
@@ -210,5 +222,7 @@ def update_schedule(uid: str, changes: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
def delete_schedule(uid: str) -> None:
|
||||
get_table("instance_schedules").delete(uid=uid)
|
||||
def delete_schedule(uid: str, deleted_by: str = "system") -> None:
|
||||
get_table("instance_schedules").update(
|
||||
{"uid": uid, "deleted_at": now(), "deleted_by": deleted_by}, ["uid"]
|
||||
)
|
||||
|
||||
@@ -44,7 +44,12 @@ class LessonStore:
|
||||
def _ensure_indexes(self) -> None:
|
||||
if TABLE not in self._db.tables:
|
||||
return
|
||||
self._db[TABLE].create_index(["owner_kind", "owner_id"])
|
||||
table = self._db[TABLE]
|
||||
if not table.has_column("deleted_at"):
|
||||
table.create_column_by_example("deleted_at", "")
|
||||
if not table.has_column("deleted_by"):
|
||||
table.create_column_by_example("deleted_by", "")
|
||||
table.create_index(["owner_kind", "owner_id"])
|
||||
|
||||
@property
|
||||
def _table(self) -> Any:
|
||||
@@ -57,7 +62,7 @@ class LessonStore:
|
||||
def count(self) -> int:
|
||||
if TABLE not in self._db.tables:
|
||||
return 0
|
||||
return self._table.count(**self._scope)
|
||||
return self._table.count(deleted_at=None, **self._scope)
|
||||
|
||||
def add(
|
||||
self, observation: str, conclusion: str, next_action: str, tags: str = ""
|
||||
@@ -70,6 +75,8 @@ class LessonStore:
|
||||
"tags": tags,
|
||||
"created_at": to_iso(now_utc()),
|
||||
"hits": 0,
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
**self._scope,
|
||||
}
|
||||
self._table.insert(record)
|
||||
@@ -82,14 +89,24 @@ class LessonStore:
|
||||
def all(self) -> list[dict[str, Any]]:
|
||||
if TABLE not in self._db.tables:
|
||||
return []
|
||||
return list(self._table.find(**self._scope))
|
||||
return list(self._table.find(deleted_at=None, **self._scope))
|
||||
|
||||
def delete(self, uid: str) -> bool:
|
||||
if TABLE not in self._db.tables:
|
||||
return False
|
||||
deleted = self._table.delete(uid=uid, **self._scope)
|
||||
row = self._table.find_one(uid=uid, deleted_at=None, **self._scope)
|
||||
if not row:
|
||||
return False
|
||||
self._table.update(
|
||||
{
|
||||
"id": row["id"],
|
||||
"deleted_at": to_iso(now_utc()),
|
||||
"deleted_by": f"{self._owner_kind}:{self._owner_id}",
|
||||
},
|
||||
["id"],
|
||||
)
|
||||
self._dirty = True
|
||||
return bool(deleted)
|
||||
return True
|
||||
|
||||
def clear(self) -> int:
|
||||
n = self.count()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import dataset
|
||||
@@ -34,6 +35,10 @@ class TaskStore:
|
||||
if TABLE not in self._db.tables:
|
||||
return
|
||||
table = self._db[TABLE]
|
||||
if not table.has_column("deleted_at"):
|
||||
table.create_column_by_example("deleted_at", "")
|
||||
if not table.has_column("deleted_by"):
|
||||
table.create_column_by_example("deleted_by", "")
|
||||
for columns in INDEXED_COLUMNS:
|
||||
table.create_index(columns)
|
||||
|
||||
@@ -46,7 +51,9 @@ class TaskStore:
|
||||
return {"owner_kind": self._owner_kind, "owner_id": self._owner_id}
|
||||
|
||||
def create(self, record: dict[str, Any]) -> None:
|
||||
self._table.insert({**record, **self._scope})
|
||||
self._table.insert(
|
||||
{"deleted_at": None, "deleted_by": None, **record, **self._scope}
|
||||
)
|
||||
logger.info(
|
||||
"Task created uid=%s owner=%s/%s",
|
||||
record.get("uid"),
|
||||
@@ -55,12 +62,13 @@ class TaskStore:
|
||||
)
|
||||
|
||||
def get(self, uid: str) -> dict[str, Any] | None:
|
||||
return self._table.find_one(uid=uid, **self._scope)
|
||||
return self._table.find_one(uid=uid, deleted_at=None, **self._scope)
|
||||
|
||||
def list(
|
||||
self, enabled_only: bool = False, status: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
criteria: dict[str, Any] = dict(self._scope)
|
||||
criteria["deleted_at"] = None
|
||||
if enabled_only:
|
||||
criteria["enabled"] = True
|
||||
if status:
|
||||
@@ -73,14 +81,25 @@ class TaskStore:
|
||||
logger.debug("Task updated uid=%s changes=%s", uid, list(changes))
|
||||
|
||||
def delete(self, uid: str) -> bool:
|
||||
deleted = self._table.delete(uid=uid, **self._scope)
|
||||
logger.info("Task deleted uid=%s ok=%s", uid, deleted)
|
||||
return bool(deleted)
|
||||
row = self._table.find_one(uid=uid, deleted_at=None, **self._scope)
|
||||
if not row:
|
||||
return False
|
||||
self._table.update(
|
||||
{
|
||||
"uid": uid,
|
||||
"deleted_at": datetime.now(timezone.utc).isoformat(),
|
||||
"deleted_by": f"{self._owner_kind}:{self._owner_id}",
|
||||
**self._scope,
|
||||
},
|
||||
["uid", "owner_kind", "owner_id"],
|
||||
)
|
||||
logger.info("Task soft-deleted uid=%s", uid)
|
||||
return True
|
||||
|
||||
def recover_running(self) -> int:
|
||||
if TABLE not in self._db.tables:
|
||||
return 0
|
||||
stuck = list(self._table.find(status="running", **self._scope))
|
||||
stuck = list(self._table.find(status="running", deleted_at=None, **self._scope))
|
||||
for row in stuck:
|
||||
self._table.update(
|
||||
{"uid": row["uid"], "status": "pending", **self._scope},
|
||||
@@ -91,7 +110,9 @@ class TaskStore:
|
||||
return len(stuck)
|
||||
|
||||
def due(self, now_iso: str) -> list[dict[str, Any]]:
|
||||
rows = self._table.find(enabled=True, status="pending", **self._scope)
|
||||
rows = self._table.find(
|
||||
enabled=True, status="pending", deleted_at=None, **self._scope
|
||||
)
|
||||
return [
|
||||
row
|
||||
for row in rows
|
||||
|
||||
@@ -46,6 +46,10 @@ class VirtualToolStore:
|
||||
if TABLE not in self._db.tables:
|
||||
return
|
||||
table = self._db[TABLE]
|
||||
if not table.has_column("deleted_at"):
|
||||
table.create_column_by_example("deleted_at", "")
|
||||
if not table.has_column("deleted_by"):
|
||||
table.create_column_by_example("deleted_by", "")
|
||||
for columns in INDEXED_COLUMNS:
|
||||
table.create_index(columns)
|
||||
|
||||
@@ -60,16 +64,23 @@ class VirtualToolStore:
|
||||
def find(self, name: str) -> dict[str, Any] | None:
|
||||
if TABLE not in self._db.tables:
|
||||
return None
|
||||
return self._table.find_one(name=name, **self._scope)
|
||||
return self._table.find_one(name=name, deleted_at=None, **self._scope)
|
||||
|
||||
def list(self) -> list[dict[str, Any]]:
|
||||
if TABLE not in self._db.tables:
|
||||
return []
|
||||
return list(self._table.find(**self._scope))
|
||||
return list(self._table.find(deleted_at=None, **self._scope))
|
||||
|
||||
def create(self, record: dict[str, Any]) -> dict[str, Any]:
|
||||
now = _now_iso()
|
||||
record = {**record, **self._scope, "created_at": now, "updated_at": now}
|
||||
record = {
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
**record,
|
||||
**self._scope,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
self._table.insert(record)
|
||||
logger.info(
|
||||
"Virtual tool created name=%s owner=%s/%s",
|
||||
@@ -85,9 +96,20 @@ class VirtualToolStore:
|
||||
logger.debug("Virtual tool updated name=%s changes=%s", name, list(changes))
|
||||
|
||||
def delete(self, name: str) -> bool:
|
||||
deleted = self._table.delete(name=name, **self._scope)
|
||||
logger.info("Virtual tool deleted name=%s ok=%s", name, deleted)
|
||||
return bool(deleted)
|
||||
row = self._table.find_one(name=name, deleted_at=None, **self._scope)
|
||||
if not row:
|
||||
return False
|
||||
self._table.update(
|
||||
{
|
||||
"name": name,
|
||||
"deleted_at": _now_iso(),
|
||||
"deleted_by": f"{self._owner_kind}:{self._owner_id}",
|
||||
**self._scope,
|
||||
},
|
||||
["name", "owner_kind", "owner_id"],
|
||||
)
|
||||
logger.info("Virtual tool soft-deleted name=%s", name)
|
||||
return True
|
||||
|
||||
def tool_schemas(self) -> list[dict[str, Any]]:
|
||||
return [_schema_for(row) for row in self.list() if row.get("enabled", 1)]
|
||||
|
||||
@@ -239,6 +239,8 @@ class NewsService(BaseService):
|
||||
"author": article.get("author", ""),
|
||||
"article_published": article.get("published", ""),
|
||||
"synced_at": now,
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
)
|
||||
new_count += 1
|
||||
@@ -296,6 +298,8 @@ class NewsService(BaseService):
|
||||
"news_uid": article_uid,
|
||||
"url": img["url"],
|
||||
"alt_text": img.get("alt_text", ""),
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user