Documentation / @zerotal/admin / index / Resource
Abstract Class: Resource
Defined in: admin/src/Resource.ts:142
@zerotal/admin — a declarative, server-driven admin panel built on @zerotal/flow (reactivity) and @zerotal/flow-ui (components).
Quick start:
// bootstrap/providers.ts import { FlowProvider } from "@zerotal/flow"; import { AdminProvider } from "@zerotal/admin"; export default [FlowProvider, AdminProvider];
// app/admin.ts import { Panel, Resource, text } from "@zerotal/admin"; import { User } from "./models/User.ts";
Panel.configure({ brand: "Acme", path: "/admin" });
class UserResource extends Resource { static model = User; static navigationIcon = "users"; static navigationGroup = "Access"; static columns() { return [ text("id").sortable(), text("name").searchable().sortable(), text("email").searchable(), text("role").badge((v) => (v === "admin" ? "primary" : "muted")), ]; } }
Panel.register(UserResource);
The default UI ships with light + dark mode out of the box (Tailwind via CDN
for now; swap to a real build later by editing theme.ts only).
Constructors
Constructor
new Resource():
Resource
Returns
Resource
Properties
model
staticmodel:AdminModel
Defined in: admin/src/Resource.ts:144
The ORM model class this resource manages.
slug?
staticoptionalslug?:string
Defined in: admin/src/Resource.ts:147
URL slug; defaults to a kebab-cased plural of the model name.
label?
staticoptionallabel?:string
Defined in: admin/src/Resource.ts:149
Singular label; defaults to the model name.
pluralLabel?
staticoptionalpluralLabel?:string
Defined in: admin/src/Resource.ts:151
Plural label; defaults to the pluralized label.
cluster?
staticoptionalcluster?: typeofCluster
Defined in: admin/src/Resource.ts:157
The Cluster this resource belongs to. Members share the cluster's URL segment and sit under one sidebar entry.
parent?
staticoptionalparent?:object
Defined in: admin/src/Resource.ts:171
Nest this resource under a parent record, so its pages live at
/admin/posts/7/comments rather than /admin/comments. Every list query is
scoped to the parent by foreignKey, and new records inherit it.
static parent = { resource: () => PostResource, foreignKey: "post_id" };
The parent is named by a function because the two resources almost always
reference each other — the parent lists the child as a relation, the child
names the parent here — and a direct reference would resolve to undefined
on whichever side the module cycle happened to evaluate first.
resource
resource: () => typeof
Resource
Returns
typeof Resource
foreignKey
foreignKey:
string
singular
staticsingular:boolean=false
Defined in: admin/src/Resource.ts:184
Back a single row rather than a collection — site settings, a company
profile. The resource mounts one route (/admin/settings) that opens the
edit form directly; there is no list, no create page and no record id. The
row is resolved by singularRecord, which creates it on first use.
navigationIcon
staticnavigationIcon:string="collection"
Defined in: admin/src/Resource.ts:187
Navigation icon key (see ui/icons.ts).
navigationGroup?
staticoptionalnavigationGroup?:string
Defined in: admin/src/Resource.ts:189
Optional sidebar group heading.
navigationParentItem?
staticoptionalnavigationParentItem?:string
Defined in: admin/src/Resource.ts:191
Nest this item under another resource's nav label.
navigationSort
staticnavigationSort:number=0
Defined in: admin/src/Resource.ts:193
Sort order within the sidebar (lower = higher).
navigationBadgeColor
staticnavigationBadgeColor:"success"|"primary"|"destructive"|"muted"="primary"
Defined in: admin/src/Resource.ts:196
Tone for the sidebar navigation badge.
perPage
staticperPage:number=15
Defined in: admin/src/Resource.ts:210
Default page size for the list table.
primaryKey
staticprimaryKey:string="id"
Defined in: admin/src/Resource.ts:213
Primary-key column, used to build View links and to find/delete records.
defaultSort?
staticoptionaldefaultSort?:object
Defined in: admin/src/Resource.ts:216
Initial sort applied when the URL doesn't specify one.
column
column:
string
direction?
optionaldirection?:"asc"|"desc"
eager
staticeager:string[] =[]
Defined in: admin/src/Resource.ts:219
Relations to eager-load for list + view (so columns/entries can read them).
defaultGroup?
staticoptionaldefaultGroup?:string
Defined in: admin/src/Resource.ts:237
Grouping applied by default (a column key from groups).
reorderable?
staticoptionalreorderable?:string
Defined in: admin/src/Resource.ts:244
Enable drag-style row reordering by persisting a position column. Set to the
integer column that stores order (e.g. "sort"); the list page then shows
up/down reorder controls and orders by this column.
recordTitleAttribute?
staticoptionalrecordTitleAttribute?:string
Defined in: admin/src/Resource.ts:247
Column used to title a record in global search, breadcrumbs, etc.
history
statichistory:boolean=false
Defined in: admin/src/Resource.ts:320
Show a history of changes on the record page, read from @zerotal/audit.
Requires the model to compose Auditable; without it there is nothing
recorded to show. An update can be put back from there.
impersonatable
staticimpersonatable:boolean=false
Defined in: admin/src/Resource.ts:327
Allow operators to act as one of these records — a user resource, in
practice. Off by default: can() defaults to allowing, which is the wrong
default for becoming somebody else, so this carries the refusal.
optimisticLock?
staticoptionaloptimisticLock?:string
Defined in: admin/src/Resource.ts:336
Guard against two people saving the same record over each other.
Names the column holding the row's version — updated_at in almost every
schema. The edit form carries the value it loaded, and a save whose value no
longer matches is refused rather than silently overwriting the other change.
treeParentColumn?
staticoptionaltreeParentColumn?:string
Defined in: admin/src/Resource.ts:344
Render this resource as a tree, nesting each record under its parent.
Names the self-referencing column. The list orders and indents by depth, so categories or an org chart read as the shape they are.
translatable
statictranslatable:string[] =[]
Defined in: admin/src/Resource.ts:352
Fields stored per locale, as { en: "…", fr: "…" } JSON columns.
The form and table show one locale at a time, switched from the list; the stored shape is unchanged, so nothing outside the panel has to know.
locales
staticlocales:string[]
Defined in: admin/src/Resource.ts:355
Locales offered when a resource is translatable.
tableLayout
statictableLayout:"table"|"calendar"|"grid"|"kanban"="table"
Defined in: admin/src/Resource.ts:366
How the list renders its records.
"table" is right for data you scan and compare. "grid" suits records you
recognise by sight — products, media, people — where a thumbnail and a name
beat a row of columns.
kanbanColumn?
staticoptionalkanbanColumn?:string
Defined in: admin/src/Resource.ts:369
Column a kanban board groups its lanes by — a status, usually.
kanbanLanes
statickanbanLanes:Record<string,string> ={}
Defined in: admin/src/Resource.ts:372
Lane order and labels for a kanban board, keyed by the column's values.
calendarColumn?
staticoptionalcalendarColumn?:string
Defined in: admin/src/Resource.ts:375
Date column a calendar lays records out on.
striped
staticstriped:boolean=false
Defined in: admin/src/Resource.ts:378
Shade alternating rows. Helps the eye track across a wide table.
stickyHeader
staticstickyHeader:boolean=false
Defined in: admin/src/Resource.ts:381
Keep the header visible while the body scrolls.
density
staticdensity:"comfortable"|"compact"="comfortable"
Defined in: admin/src/Resource.ts:384
Row height. "compact" fits noticeably more on screen.
filterLayout
staticfilterLayout:"inline"|"panel"|"drawer"="inline"
Defined in: admin/src/Resource.ts:393
Where the filters sit.
"inline" keeps them above the table, which is fine for two or three.
"panel" collapses them behind a Filters button, and "drawer" slides them
in from the side — both worth it once filters outnumber the space for them.
Methods
parentResource()
staticparentResource(): typeofResource|undefined
Defined in: admin/src/Resource.ts:174
The parent resource, resolved.
Returns
typeof Resource | undefined
navigationBadge()
staticnavigationBadge():string|number|Promise<string|number|null> |null
Defined in: admin/src/Resource.ts:205
A count or label shown beside this item in the sidebar. Return
null/undefined for no badge. Resolved on each render, so cache an
expensive count rather than paying for it on every page.
static async navigationBadge() { return this.count(); }
Returns
string | number | Promise<string | number | null> | null
tabs()
statictabs():Tab[]
Defined in: admin/src/Resource.ts:222
List-page filter tabs. Override to add them.
Returns
Tab[]
filters()
staticfilters():Filter[]
Defined in: admin/src/Resource.ts:227
List-page filters. Override to add them.
Returns
Filter[]
groups()
staticgroups():Group[]
Defined in: admin/src/Resource.ts:232
Row groupings offered on the list table.
Returns
Group[]
recordTitle()
staticrecordTitle(record):string
Defined in: admin/src/Resource.ts:250
Resolve a human-readable title for a record.
Parameters
record
Record<string, unknown>
Returns
string
globallySearchable()
staticgloballySearchable():boolean
Defined in: admin/src/Resource.ts:257
Whether this resource participates in global search (has searchable columns).
Returns
boolean
relations()
staticrelations():RelationManager[]
Defined in: admin/src/Resource.ts:262
Relation managers shown on the View page.
Returns
columns()
staticcolumns():Column[]
Defined in: admin/src/Resource.ts:267
Table columns. Override in the subclass.
Returns
Column[]
infolist()
staticinfolist():InfolistComponent[]
Defined in: admin/src/Resource.ts:276
View-page schema — an infolist: an ordered list of
Sections and/or entries. Override to customize the detail page;
when left empty it falls back to a single section derived from columns().
Returns
form()
staticform():FormComponent[]
Defined in: admin/src/Resource.ts:285
Form schema for the Create/Edit pages: an ordered list of
Fields. Use .visibleOn("create") / .hiddenOn("edit") to vary a
field by page. An empty list disables Create/Edit for the resource.
Returns
isEditable()
staticisEditable():boolean
Defined in: admin/src/Resource.ts:290
Whether this resource exposes Create/Edit pages (any fields defined).
Returns
boolean
recordActions()
staticrecordActions():ActionItem[]
Defined in: admin/src/Resource.ts:300
Per-row actions. Defaults to View + Edit + Delete; override to customize.
Returns
headerActions()
staticheaderActions():ActionItem[]
Defined in: admin/src/Resource.ts:305
Actions shown above the table (defaults to a Create button).
Returns
bulkActions()
staticbulkActions():ActionItem[]
Defined in: admin/src/Resource.ts:310
Actions applied to the selected rows (defaults to bulk Delete).
Returns
widgets()
staticwidgets():DashboardWidget[]
Defined in: admin/src/Resource.ts:407
Widgets shown above this resource's table, and on its record pages.
The dashboard answers "how is the business doing"; these answer "what is
going on in this list" — a pending count above the orders table, a revenue
chart above products. Same widget builders as the dashboard, so .poll()
works here too.
static widgets() { return [statsWidget(async () => [stat("Pending", await Order.pending())])]; }
Returns
emptyState()
staticemptyState():EmptyState
Defined in: admin/src/Resource.ts:429
What a user sees instead of a table when there is nothing to show.
A blank table teaches nobody anything. Override this to say why the list is empty and what to do about it — the difference between "No records" and "No orders yet. They'll appear here once a customer checks out."
static emptyState() { return { heading: "No orders yet", description: "Orders appear here as soon as a customer checks out.", icon: "inbox", }; }
A search or filter that matches nothing gets a different, automatic message — this is for a genuinely empty resource.
Returns
can()
staticcan(_ability,_record?):boolean
Defined in: admin/src/Resource.ts:447
Authorization gate (policies). Returns true by default; override to
enforce permissions, e.g. delegate to @zerotal/auth's Gate:
static can(ability: string, record?: AdminRecord) { return Gate.allows(ability, record ?? this.model); }
Abilities used by the built-in actions: create, update, delete,
restore, forceDelete.
Parameters
_ability
string
_record?
Returns
boolean
mutateFormDataBeforeFill()
staticmutateFormDataBeforeFill(data):AdminRecord
Defined in: admin/src/Resource.ts:454
Transform a record into form state before the Edit form is filled.
Parameters
data
Returns
mutateBeforeSave()
staticmutateBeforeSave(data,_mode):AdminRecord
Defined in: admin/src/Resource.ts:459
Transform validated form data just before create/update.
Parameters
data
_mode
"create" | "edit"
Returns
afterSave()
staticafterSave(_record,_mode):Promise<void>
Defined in: admin/src/Resource.ts:464
Hook fired after a successful create/update (e.g. sync relations).
Parameters
_record
_mode
"create" | "edit"
Returns
Promise<void>
getModelName()
staticgetModelName():string
Defined in: admin/src/Resource.ts:468
Returns
string
getLabel()
staticgetLabel():string
Defined in: admin/src/Resource.ts:472
Returns
string
getPluralLabel()
staticgetPluralLabel():string
Defined in: admin/src/Resource.ts:476
Returns
string
getSlug()
staticgetSlug():string
Defined in: admin/src/Resource.ts:480
Returns
string
arrangeTree()
staticarrangeTree(rows):object[]
Defined in: admin/src/Resource.ts:493
Order rows so each sits under its parent, and report how deep each one is.
Done in memory over the page's rows rather than in SQL: a recursive CTE is the right answer for a deep tree, but it is not portable across the drivers the panel supports, and a tree small enough to browse is small enough to arrange here. Orphans — rows whose parent is not in the set — are kept at the top level rather than dropped, so a filtered tree never hides records.
Parameters
rows
Record<string, unknown>[]
Returns
object[]
translated()
statictranslated(value,locale):unknown
Defined in: admin/src/Resource.ts:536
Read a translatable field for one locale.
A translatable column stores { en: "…", fr: "…" }; a value that was never
translated is returned as-is, so turning translation on for an existing
column does not blank it.
Parameters
value
unknown
locale
string
Returns
unknown
routePath()
staticroutePath():string
Defined in: admin/src/Resource.ts:551
The route pattern for this resource's index, relative to the panel base and
with any parent id still a :param placeholder.
Returns
string
parentParam()
staticparentParam():string
Defined in: admin/src/Resource.ts:562
Route-parameter name carrying the parent record's id, for a nested resource.
Returns
string
indexUrl()
staticindexUrl(base,parentId?):string
Defined in: admin/src/Resource.ts:570
This resource's index URL under a panel base. Nested resources need the parent record's id; passing none leaves the placeholder in place.
Parameters
base
string
parentId?
unknown
Returns
string
recordUrl()
staticrecordUrl(base,id,parentId?):string
Defined in: admin/src/Resource.ts:579
Parameters
base
string
id
unknown
parentId?
unknown
Returns
string
createUrl()
staticcreateUrl(base,parentId?):string
Defined in: admin/src/Resource.ts:583
Parameters
base
string
parentId?
unknown
Returns
string
editUrl()
staticeditUrl(base,id,parentId?):string
Defined in: admin/src/Resource.ts:587
Parameters
base
string
id
unknown
parentId?
unknown
Returns
string
singularRecord()
staticsingularRecord():Promise<Record<string,unknown> |null>
Defined in: admin/src/Resource.ts:597
Resolve the single row a singular resource edits, creating it from the form's defaults when it doesn't exist yet.
Returns
Promise<Record<string, unknown> | null>
searchableColumns()
staticsearchableColumns():string[]
Defined in: admin/src/Resource.ts:609
Database columns flagged .searchable() (honours .column() overrides).
Returns
string[]
data()
staticdata():Record<string,unknown>[] |Promise<Record<string,unknown>[] |null> |null
Defined in: admin/src/Resource.ts:633
Rows for a resource that isn't backed by an ORM model — an external API, a config file, a computed report.
Return the full set; the panel filters, sorts and paginates it in memory,
so search, tabs, summaries and the query builder keep working. Returning
null (the default) means "use model", which is the normal case.
static async data() { return (await fetch("https://api.example.com/regions").then((r) => r.json())); }
Writes are a separate question: a read-only source needs no form(), and a
writable one overrides create, update and destroy to push changes back
wherever they belong.
Returns
Record<string, unknown>[] | Promise<Record<string, unknown>[] | null> | null
records()
staticrecords(options?):Promise<RecordPage>
Defined in: admin/src/Resource.ts:641
Load a page of records honoring search, sort, and pagination. Defensive about the ORM surface so resources keep working under partial mocks/tests.
Parameters
options?
ListOptions = {}
Returns
Promise<RecordPage>
listAll()
staticlistAll(options?):Promise<Record<string,unknown>[]>
Defined in: admin/src/Resource.ts:716
Load all rows matching the current scope (search + tab/filters + trashed),
with no pagination — used for column summaries and reorder swaps. Mirrors the
scoping of records but skips limit/offset.
Parameters
options?
ListOptions = {}
Returns
Promise<Record<string, unknown>[]>
count()
staticcount(modifyQuery?):Promise<number>
Defined in: admin/src/Resource.ts:769
Count records, optionally scoped by a query modifier (used for tab badges).
Parameters
modifyQuery?
Returns
Promise<number>
find()
staticfind(id):Promise<Record<string,unknown> |null>
Defined in: admin/src/Resource.ts:785
Load a single record by primary key, as a plain row (or null). Uses the
query builder so the row shape matches records; falls back to the
in-memory model surface used by tests/mocks.
Parameters
id
unknown
Returns
Promise<Record<string, unknown> | null>
usesSoftDeletes()
staticusesSoftDeletes():boolean
Defined in: admin/src/Resource.ts:813
Whether this resource's model uses soft deletes.
Returns
boolean
restore()
staticrestore(id):Promise<boolean>
Defined in: admin/src/Resource.ts:832
Restore a soft-deleted record. Returns true when a record was restored.
Parameters
id
unknown
Returns
Promise<boolean>
forceDelete()
staticforceDelete(id):Promise<boolean>
Defined in: admin/src/Resource.ts:840
Permanently delete a (possibly soft-deleted) record.
Parameters
id
unknown
Returns
Promise<boolean>
destroy()
staticdestroy(id):Promise<boolean>
Defined in: admin/src/Resource.ts:854
Permanently delete a record by primary key. Prefers loading the model
instance and calling its delete() (so soft-deletes / model hooks run);
returns true when a matching record was found and removed.
Parameters
id
unknown
Returns
Promise<boolean>
create()
staticcreate(data):Promise<AdminRecord|null>
Defined in: admin/src/Resource.ts:866
Create a record from form data. Returns the new record (with its id).
Parameters
data
Record<string, unknown>
Returns
Promise<AdminRecord | null>
update()
staticupdate(id,data):Promise<boolean>
Defined in: admin/src/Resource.ts:875
Update a record by primary key from form data. Returns true on success.
Parameters
id
unknown
data
Record<string, unknown>
Returns
Promise<boolean>