Skip to content

angee.graphql.deletion

GraphQL delete-confirmation preview built on Django's deletion collector.

DeletePreviewGroup

python
@strawberry.type
class DeletePreviewGroup()

A count of affected rows for one Django model.

label

Human-readable plural model label.

count

Number of affected rows for this model.

DeletePreviewNode

python
@strawberry.type
class DeletePreviewNode()

One node in a cascade deletion preview tree.

label

Human-readable model or group label.

object_label

Human-readable object display label.

object_id

Public object id when the node represents a concrete row.

children

Child nodes below this preview node.

from_row

python
@classmethod
def from_row(cls, instance: models.Model) -> DeletePreviewNode

Return a childless leaf node for one deleted row.

from_group

python
@classmethod
def from_group(cls, model: type[models.Model],
               rows: _PreviewRows) -> DeletePreviewNode

Return a grouped child node summarizing deleted rows of one model.

from_target

python
@classmethod
def from_target(
        cls, instance: models.Model,
        groups: dict[type[models.Model], _PreviewRows]) -> DeletePreviewNode

Return the root node (the deletion target) with its grouped children.

DeletePreview

python
@strawberry.type
class DeletePreview()

Cascade forecast for deleting one Django model instance.

total_deleted_count

Total number of rows Django would delete.

deleted

Rows Django would delete.

updated

Rows Django would update because of on_delete behavior.

blocked

Rows whose on_delete behavior blocks deletion.

has_blockers

Whether any related rows block deletion (i.e. blocked is non-empty).

root

Rooted tree of rows Django would delete; deleted counts include this root row.

deleted_instance

Deleted row returned internally to mutation envelopes, never exposed in SDL.

from_instance

python
@classmethod
def from_instance(cls,
                  instance: models.Model,
                  actor: Any | None = None) -> DeletePreview

Return Django's cascade forecast for instance.

Callers should run previews inside the same transaction as the eventual delete so fast-delete counts and visible rows share one database snapshot.

from_counts

python
@classmethod
def from_counts(cls, target: models.Model,
                counts: Mapping[type[models.Model], int]) -> DeletePreview

Return a delete forecast whose non-root rows are known by count, not collected.

:meth:from_instance runs Django's Collector, which materializes every related row — including the on_delete=SET_NULL rows it would only update. When a delete is really a purge that removes those rows through a separate bulk scoped queryset.delete() (a messaging channel's threads and messages FK the shared integrate.Integration parent with SET_NULL, so the collector would forecast them as updated and load all of them), the purge owner counts the scope with .count() and passes the totals here instead. This builds the same preview shape with no per-row materialization — a fixed number of queries regardless of how large the purged scope is.

target is the tree root: it counts as one deleted row of its own model, and each of its multi-table-inheritance parents (target._meta.parents — e.g. a channel's Integration row) as one more, since deleting an MTI child deletes its parent rows too. counts names the additional purged models and their totals — the deletion scope including the CASCADE subtrees (for a channel: its threads and messages plus their parts, reactions, participants, followers, activities, notifications, edges, stars, and tracking values) — so summing them equals the rows the delete removes. Any type(target) entry in counts is ignored so a caller that re-counts the root cannot double it against the root's own +1.

Unlike :meth:from_instance, counts is caller-trusted and unscoped: the purge owner computes it elevated, so the preview reflects the true scope regardless of the actor's REBAC row visibility. The count-based path assumes no blockers — a channel purge has none today — so it takes the has_blockers=False branch; a blocker only surfaces if the confirm delete actually raises, whose owner rebuilds the preview through :meth:blocked_from_error.

blocked_from_error

python
@classmethod
def blocked_from_error(
        cls, target: models.Model,
        error: ProtectedError | RestrictedError) -> DeletePreview

Return a preview surfacing the rows that blocked a purge's confirm delete.

The count-based :meth:from_counts path assumes no blockers. If a consumer addon ever PROTECT\s or RESTRICT\s a reverse FK to target, the confirm delete raises instead of orphaning; the purge owner catches it and calls this so the surface reports the blockers as a blocked preview (mirroring :func:delete_by_public_id), never a raw 500. Nothing was deleted — the purge transaction rolled back — so the deleted/updated counts are empty and only blocked is populated, straight from the offending rows the error carries.

delete_by_public_id

python
def delete_by_public_id(model: type[models.Model],
                        public_id: str,
                        *,
                        confirm: bool = False,
                        queryset: models.QuerySet[models.Model] | None = None,
                        before_delete: Callable[[models.Model], None]
                        | None = None,
                        reason: str | None = None) -> DeletePreview

Preview, then optionally delete, one public-id-addressed model row.

The caller owns authorization. Passing reason elevates the lookup/delete under system_context for admin/action surfaces whose permission class has already gated the request actor.

attach_delete_preview_metadata

python
def attach_delete_preview_metadata(
        surface: type[_SurfaceT],
        *,
        model: type[models.Model],
        node: type,
        field: str,
        model_label: str | None = None,
        public_id_field: str = PUBLIC_ID_FIELD_NAME) -> type[_SurfaceT]

Attach resource metadata for one authored cascade-preview mutation.

_PreviewRows

python
@dataclass(slots=True)
class _PreviewRows()

Access-scoped visible/count-only row state for one preview tree group.

add

python
def add(*, total_count: int, visible_count: int,
        visible_rows: Iterable[models.Model]) -> None

Merge one collector source into this model group.

by_model

python
@classmethod
def by_model(cls, root: models.Model, collector: Collector,
             fast_deletes: tuple[_FastDelete, ...],
             actor: Any | None) -> dict[type[models.Model], _PreviewRows]

Return deleted tree rows grouped by model, excluding root.

from_collected

python
@classmethod
def from_collected(cls, root: models.Model, model: type[models.Model],
                   rows: Iterable[models.Model],
                   actor: Any | None) -> _PreviewRows

Return access-scoped preview rows from collector-materialized rows.

from_fast_delete

python
@classmethod
def from_fast_delete(cls, queryset: models.QuerySet[models.Model],
                     total_count: int, actor: Any | None) -> _PreviewRows

Return access-scoped preview rows from one fast-delete queryset.

Released under the AGPL-3.0 License.