angee.graphql.data.hasura
Angee metadata bridge for strawberry-django-hasura resources.
HasuraLines
@dataclass(frozen=True)
class HasuraLines()A declared editable child-lines relation for a document resource (F6).
A resource passes lines=HasuraLines(field="lines", model=OrderLine) to :func:hasura_model_resource to gain (a) Hasura-native nested inserts (insert_<res>_one(object: {..., lines: {data: [...]}}), riding the strawberry-django-hasura nested-insert shape) and (b) an authored <res>_save(pk, patch, lines) mutation that diff-applies children (create/update/delete by public id) and patches the parent in one transaction, REBAC-checked on the parent (children ride the §3.4 elevation after that preflight).
field is the parent's reverse-FK accessor to the child rows; the child's FK back to the parent is derived from it and set by the write, never asked for on the wire. writable overrides the child's editable-column allowlist; public_id_fields names the child relation columns exposed as public ids (decoded on write). node is the child GraphQL node, used only to name the child field metadata the frontend line cells render. position_field names the integer order column (advertised so the composer maintains it).
Completeness contract: <res>_save(lines=…) takes the full desired child set — deletion is by omission, so an id absent from the set is deleted. The caller must therefore send back every stored line (each kept row carrying its public id); a partial read that omits stored lines would ask to delete them. The write enforces the enforceable half of this contract server-side: every public id the caller sends must address a currently stored line of this parent (a stale, foreign, or truncated baseline is rejected wholesale with a ValidationError rather than silently mis-applied). The full desired set is resolved under a parent-row lock so concurrent saves cannot cross-delete each other's lines.
AngeeHasuraWriteBackend
class AngeeHasuraWriteBackend()Authorized write backend for Angee Hasura resources.
strawberry-django-hasura owns the Hasura mutation envelope. This class owns the Angee write semantics inside that envelope: Django validation, REBAC row-scoped write targets, model save/delete signals, and returning a deleted instance in Hasura's delete_<res>_by_pk shape.
write_target_queryset
def write_target_queryset() -> models.QuerySet[Any]Return the queryset that resolves this backend's update/delete/save targets.
Defaults to the model's write-scoped queryset (REBAC row scope kept, field-read redaction off). A subclass narrows it to keep rows a surface must never reach by pk off the generic update/delete/save mutations — even when the row's own REBAC would allow the write. Record-attached chatter, isolated to the record-scoped record_thread surface, is the motivating case: its own owner/admin permission would otherwise let a creator who lost record access delete the thread through delete_<res>_by_pk.
create
def create(info: strawberry.Info, data: dict[str, Any]) -> AnyCreate one row (and any declared nested child lines) atomically.
save
def save(info: strawberry.Info, pk: str, patch: dict[str, Any],
line_rows: list[dict[str, Any]] | None) -> AnyPatch one parent and diff-apply its child lines in one transaction.
REBAC preflight is on the parent, unconditionally: the row is loaded through the write-scoped queryset (field-read redaction off, REBAC row scope still evaluating read), so an actor who may read but not write the parent still resolves the row — the explicit has_access("write") gate below is what denies them. That gate must run even when patch is empty: a lines-only edit (patch={}, the FormView shape) skips the update resolver's write signal, so without the preflight the §3.4 child elevation would run unauthorized. Only after the parent write is verified do the children ride the elevation — created, updated, and deleted under system_context, authorized by the parent write, not per child row. line_rows is the full desired child set: None leaves the lines untouched (a parent-only save), an empty list clears them.
update
def update(info: strawberry.Info, pk: str, data: dict[str, Any]) -> AnyPatch one public-id-addressed row through the write queryset.
delete
def delete(info: strawberry.Info, pk: str) -> Any | NoneDelete one public-id-addressed row and return the deleted instance.
public_pk_decoder
def public_pk_decoder(model: type[models.Model]) -> Callable[[Any], Any]Return a decoder from Angee public id to database primary key.
aggregate_queryset
def aggregate_queryset(queryset: models.QuerySet[Any]) -> models.QuerySet[Any]Return the aggregate-safe variant of a REBAC queryset when available.
A REBAC-scoped queryset exposes scoped_for_aggregate to drop row-fanout joins before aggregation; a plain queryset has no such method and is returned unchanged. Resources with a custom aggregate source wrap it through here.
declared_hasura_resource_fields
def declared_hasura_resource_fields(model: type[models.Model],
attribute: str) -> tuple[str, ...]Return Hasura resource fields declared by a composed model or extension base.
Same-row model extensions own the fields they add and may declare which of those fields are writable/filterable/sortable on a Hasura resource by setting attribute on their source model class. The composed runtime model inherits those bases; this helper gathers only directly declared attributes from the MRO so a downstream extension can contribute without the base addon importing it.
hasura_model_resource
def hasura_model_resource(
node: type,
*,
model: type[models.Model],
name: str | None = None,
filterable: Sequence[str],
sortable: Sequence[str],
aggregatable: Sequence[str],
groupable: Sequence[str] = (),
json_paths: Mapping[str, str] | None = None,
writable: Sequence[str] | None = None,
insertable: Sequence[str] | None = None,
updatable: Sequence[str] | None = None,
lines: HasuraLines | None = None,
insert: bool = True,
update: bool = True,
delete: bool = True,
field_id_decode: Mapping[str, Callable[[Any], Any]] | None = None,
get_queryset: Callable[[strawberry.Info], models.QuerySet[Any]]
| None = None,
get_aggregate_queryset: Callable[[strawberry.Info],
models.QuerySet[Any]] | None = None,
write_backend: WriteBackend | None = None,
id_decode: Callable[[Any], Any] | None = None,
id_column: str = "pk",
declared_fields: Sequence[str | DataResourceFieldMetadata] = (),
model_label: str | None = None,
public_id_field: str = PUBLIC_ID_FIELD_NAME,
row_model: str = "server",
subtitle: DataResourceSubtitleMetadata | None = None
) -> HasuraResourceBuild a Hasura resource and attach Angee's model-resource metadata.
strawberry-django-hasura owns the portable Hasura dialect mechanics. This wrapper owns the Angee seam around that resource: attaching the Phase 1 angee.resources metadata contribution, and defaulting the standard glue a public-id model resource shares — base/aggregate querysets, the authorized write backend, and the public-id id decoder. A caller overrides any knob only where the resource's intent differs (REBAC-scoped reads, a custom write backend, a non-pk identity column).
lines=HasuraLines(field="lines", model=...) (F6) declares an editable child-lines relation: the insert surface rides the upstream nested-insert shape (insert_<res>_one(object: {..., lines: {data: [...]}})) and an authored <res>_save(pk, patch, lines) mutation diff-applies children plus patches the parent in one transaction. The default write backend becomes a lines-aware :class:AngeeHasuraWriteBackend; a caller supplying its own write_backend must make it lines-aware.
subtitle=DataResourceSubtitleMetadata(...) declares the renderer's closed created/updated/word-count vocabulary as dotted GraphQL selection paths. Every path resolves against node during metadata emission; adding another semantic fact extends the declaration and renderer together.
attach_hasura_resource_metadata
def attach_hasura_resource_metadata(
resource: HasuraResource,
*,
node: type,
model: type[models.Model],
name: str,
filterable: tuple[str, ...],
sortable: tuple[str, ...],
aggregatable: tuple[str, ...],
groupable: tuple[str, ...] = (),
json_paths: Mapping[str, str] | None = None,
insert: bool = True,
update: bool = True,
delete: bool = True,
lines: HasuraLines | None = None,
declared_fields: tuple[str | DataResourceFieldMetadata, ...] = (),
model_label: str | None = None,
public_id_field: str = PUBLIC_ID_FIELD_NAME,
row_model: str = "server",
subtitle: DataResourceSubtitleMetadata | None = None
) -> HasuraResourceAttach Angee resource metadata to a built Hasura resource bundle.