Auditing¶
Layer: Backend · a resource-level opt-in flag
Flip audit: true on a resource whose model is an append-only / EAV dimension
and codegen mounts a read-only {slug}_activity child resource — one row per
version of a record, with field-level before/after diffs — plus the FE table and
a changeset drawer to browse it. No log table, view, or proxy model by hand.
Note
Auditing is not yet merged to main — it lives on the commenting-audit
branch (checked out at fsh-5). The config surface, the x-fsh-audit
extension, and the activity resource shape may still change.
How it works¶
The append-only / EAV factories auto-install an ActivityViewPlugin that builds
a {table}_activity SQL view (one folded row per changeset). You own the parent
model’s factory and the audit: true flag; codegen’s AuditScaffold walks each
flagged resource, emits an ORM proxy over the view and an in-memory
activity resource that flows through the normal pipeline
(schemas, routes, serializers, openapi), stamping x-fsh-audit so the FE
auto-detects it and scaffolds the table + changeset catalog.
The config you write¶
You write — be/config/inventory_resources/category.jsonnet:
{
model: "inventory.models.Category",
pk: { name: "id", type: "uuid" },
route_prefix: "/categories",
representation_roles: { default: "default", opa: "default" },
// The entire opt-in. Mounts a read-only `/categories-activity` resource over
// the changeset view: get + a searchable list ref-filtered by category id,
// ordered by changed_at. Silent no-op unless the model's `__factory__` is an
// audit dimension (below) — that's what creates the view it reads.
audit: true,
operations: [ /* get, list, create, update, delete — the parent's own CRUD */ ],
}
You write — inventory/models/category.py: the parent must opt into an audit dimension factory.
from codegen_database.factory import CodegenDatabaseAppendOnly # or CodegenDatabaseEAV
class Category(Base):
__tablename__ = "inventory_categories"
__table_args__ = {"schema": "public"}
# Installs the ActivityViewPlugin -> creates `inventory_categories_activity`.
# Any other factory makes `audit: true` a no-op.
__factory__ = CodegenDatabaseAppendOnly
The frontend codegen emits¶
The scaffold appends the activity ResourceConfig in-memory — read-only, over
the view’s columns (id, {parent}_id, changed_at, actor (NULL for
historical rows), change_type 'create' | 'update', fields jsonb diff) —
and stamps audit_parent as x-fsh-audit on every route. The FE keys off that
extension (is_activity_list_op) and emits an ordinary bound table, no FE config:
Generated — fe/src/_generated/tables/inventory/category_activity.gen.ts (do not edit):
import { useFSHTableController } from "@fsh/components-library";
import { listCategoryActivity } from "../../sdk/inventory/category_activity.gen";
// Detected via x-fsh-audit; the ref filter is the parent's id.
export const categoryActivityTableFilters = [
{ id: "category_id", label: "Category" },
] as const satisfies readonly FilterDefinition[];
// use<CacheKey>Table hook baked with the SDK fn, cache key, and pagination —
// a view only declares columns. See table-configuration.md.
The one hand-editable artifact is the changeset catalog, scaffolded once
(if_exists="skip"). Codegen can’t read tracked column names out of the JSONB
fields diff, so you list them by hand — one entry per tracked column:
Scaffolded, safe to edit — fe/src/fields/inventory/categoryActivityChangeset.tsx:
import type { ChangesetFieldDef } from "@fsh/components-library";
// { field, label?, render?(value) } — feeds the FSHChangesetDrawer diff.
// Add an entry when you add a tracked column, or it renders without a label.
export const categoryActivityChangesetFields: ChangesetFieldDef[] = [
{ field: "name", label: "Name" },
{ field: "description", label: "Description" },
];