Skip to content

Resources

Updated View as Markdown

defineResource(Model) binds a Lucid model to generated CRUD routes. Its default slug is the pluralized kebab-case model name, with a trailing Model removed. Override public URLs using .slug() rather than depending on class naming.

import { defineResource, pages, table } from '@rikology/pupitre'
import Post from '#models/post'

export default defineResource(Post)
  .slug('posts').label('Post').pluralLabel('Posts')
  .recordTitle('title')
  .navigation({ group: 'Content', sort: 10 })
  .globallySearchable(['title', 'author.name'])
  .globalSearchDetails((record) => [record.status])
  .form((s) => [s.text('title').required()])
  .table((t) => t.columns([table.text('title').searchable()]))
  .pages({ list: pages.list(), create: pages.create().redirectTo('edit'), edit: pages.edit(), view: pages.view() })

form, table and infolist callbacks describe UI, not request handlers. Form names are typed against model attributes. .icon(string) sets a navigation icon. .actions({ header, row, bulk }) places actions. .policy(PolicyClass) or .policy(() => import(...)) selects authorization explicitly.

Pages

Default pages are list/create/edit. .pages({...}) replaces that map, so list every page you want to keep. pages.list/create/edit/view() return page builders supporting .component(name), .redirectTo('edit' | 'list' | 'view'), .widgets([...]) and .footerWidgets([...]). pages.list().tabs({...}) adds scoped list tabs; tab definitions can include a query modifier, icon and badge. .modals() on the list page uses create/edit action forms instead of separate create/edit routes.

Adding .infolist() automatically adds a view page if none exists. Record pages expose sub-navigation to edit/view, relation tabs and nested resources.

Persistence hooks

Hook Signature / use
fill(record, ctx) Return form data for editing
mutateBeforeCreate(data, ctx, trx) Transform create input before persistence
mutateBeforeSave(data, record, ctx, trx) Transform update input
beforeCreate, afterCreate Create lifecycle
beforeSave, afterSave Update lifecycle
beforeDelete, afterDelete Delete lifecycle
create(callback), update(callback) Replace default persistence handlers

Callbacks may be asynchronous where their type allows it. See the exact hook types for callback parameters, especially the transaction argument on custom persistence handlers. Use the supplied transaction rather than opening unrelated transactions or writing outside it.

.scope((query, ctx) => ...) applies an application query restriction. .scope(false) opts a global resource out of automatic tenant scoping; it does not remove policy checks. Do not use it for tenant-owned data.

Nested resources

Register a child with parent .nested([ChildResource]) and child .parent(ParentModelOrResource, 'relationName'). Routes include the parent record ID, queries are parent-scoped, create fills ownership, and breadcrumbs include the parent. A client-supplied foreign key is not a substitute for the server’s parent scope.

Relation managers

import { defineRelationManager, table, actions } from '@rikology/pupitre'
import Post from '#models/post'

export const Comments = defineRelationManager(Post, 'comments')
  .label('Comments')
  .form((s) => [s.textarea('body').required()])
  .table((t) => t.columns([table.text('body')]))
  .actions({ header: [actions.create()], row: [actions.edit(), actions.delete()] })

Register with resource .relations([Comments]); relationGroup(label, [...]) groups managers. Managers support icon, badge (ctx, record), read-only mode and record title. Their tables load as deferred data on edit/view pages. Use attach/detach for many-to-many and associate/dissociate for appropriate relationships. Related-model policy and parent access are checked on the server.

.softDeletes() defaults to model attribute deletedAt; pass a different attribute if needed. Add table.filter.trashed() and restore/force-delete actions. Queries remain tenant/parent-scoped, including restore and force deletion.

.globallySearchable([...]) opts the resource into panel global search, including supported dotted relation fields. .globalSearchDetails(record => string[]) supplies detail lines. Search filters resources and records through policies and chooses view/update destinations based on allowed abilities. Add indexes on searchable columns; pupitre:inspect reports missing indexes.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close