Skip to content

Entity View

EntityView is the saved-view aggregate that lets a user pin a configured browse (filter set + sort + columns + per-layout overlay like the kanban groupBy) on top of a compiled QueryDefinition. It supersedes the legacy Granit.QueryEngine.SavedViews (clean-broken in PR #1639) per ADR-047.

The aggregate enforces the three-visibility model at write time and exposes a sorted list at read time, with per-user permission filtering and audit trails on every mutation.

Per ADR-047 §4:

ScopeOwnerAudienceCreated via
PersonalThe current userOwner onlyCreate(...) — only valid initial visibility
SharedThe original creatorRoles + user IDs declared in EntityViewSharedWithShareWith(...) from a Personal view
TenantTenant adminEvery user in the tenantPromoteToTenant(...) from a Shared view

Promotion is one-way (Personal → Shared → Tenant); a Tenant view can never demote back to Personal.

{
"id": "9c8f0d20-…",
"entityName": "Granit.Parties.Party",
"basedOn": "Granit.Parties.PartyQuery",
"kind": "list", // or "kanban", "calendar", … (per ADR-042)
"name": "Active customers — APAC",
"description": null,
"icon": "globe",
"state": { // JSONB delta over the base collection
"filters": [ { "field": "Region", "op": "eq", "value": "APAC" } ],
"sort": [ "-Revenue" ],
"columns": [ "Name", "Region", "Revenue", "Status" ]
},
"visibility": "Personal",
"ownerId": "",
"isPersonalDefault": true, // user's landing view
"sortOrder": 0
}

The state field is intentionally opaque on the wire — its shape depends on kind. For kind: "kanban" it carries overlay fields like groupBy and per-column state (Open / Collapsed / Hidden) overrides on top of the framework defaults declared via KanbanView<TGroupBy>.

MapGranitEntityViewsEndpoints("/api/entities/{name}/views") mounts the full CRUD surface on the host:

MethodRoutePermissionNotes
GET/(any authenticated user)Returns Personal owned by user + Shared targeted to user + Tenant
GET/{id:guid}Same as list404 when inaccessible (no scope leak)
GET/_defaultSame as listResolves the user’s effective default per ADR-047 §4
POST/Entities.Views.CreateReturns 201
PUT/{id:guid}Owner-only for Personal/Shared, Entities.Views.Manage for TenantBasedOn and Kind immutable
DELETE/{id:guid}Owner, or Entities.Views.Delete.AnyReturns 204
POST/{id:guid}/pinEntities.Views.ManageAdmin promotes a Tenant view to a workspace-tab pin
POST/{id:guid}/set-defaultEntities.Views.ManageReplaces the compiled default for the tenant
POST/{id:guid}/star(any authenticated user)Per-user — sets IsPersonalDefault
POST/{id:guid}/shareOwnerPersonal → Shared promotion

Every write goes through IEntityViewWriter, which persists via the standard AuditedEntityInterceptor pipeline — CreatedAt / CreatedBy / ModifiedAt / ModifiedBy are recorded on every mutation, and the framework’s audit infrastructure (Granit.Auditing) captures the before/after diff.

EntityViewCreateBodyRequest, EntityViewUpdateBodyRequest and EntityViewShareBodyRequest ship FluentValidation validators discovered automatically by GranitValidationModule. Localized error codes resolve to the host’s locale via Granit:Validation:* keys (auto-mapped from FluentValidation’s PropertyValidator rule sets).

  • Entity Definition — the compiled layer that EntityView overlays
  • ADR-042 — view kinds + state schema per kind
  • ADR-047 — supersedes legacy SavedViews
  • ADR-049 — 5-tier landing route resolver (uses EntityView defaults)