Entity Actions
Actions are custom operations beyond CRUD that you can attach to entities. They're perfect for business logic like "verify email", "checkout", "publish", or "archive".
Basic Action
typescript
import { entity, action, string } from "nevr"
const user = entity("user", {
email: string,
verified: boolean.default(false),
}).actions({
verify: action()
.onResource()
.handler(async (ctx) => {
await ctx.driver.update("user", { id: ctx.resourceId }, { verified: true })
return { success: true }
}),
})Generated Route: POST /users/:id/verify
Action Builder API
The action() function returns a fluent builder:
typescript
action()
.method("POST") // HTTP method (default: POST)
.get() // Shorthand for .method("GET")
.post() // Shorthand for .method("POST")
.onResource() // Requires :id param
.rules("authenticated", "owner") // Authorization
.input({ code: string }) // Input validation
.handler(async (ctx) => {...}) // Handler function
.workflow([...]) // Workflow steps
.meta({ summary: "..." }) // OpenAPI metadataAction Types
Collection Actions (No ID)
typescript
// POST /users/invite
invite: action()
.input({ email: string })
.handler(async (ctx) => {
// Send invite email
return { sent: true }
})Resource Actions (With ID)
typescript
// POST /users/:id/verify
verify: action()
.onResource()
.handler(async (ctx) => {
// ctx.resourceId is available
// ctx.resource is the fetched entity
return { verified: true }
})GET Actions
typescript
// GET /orders/:id/track
track: action()
.get()
.onResource()
.handler(async (ctx) => {
const tracking = await ctx.resolve("shipping").getTracking(ctx.resource.shipmentId)
return tracking
})Action Context
The handler receives an EntityActionContext:
typescript
interface EntityActionContext<TInput> {
entity: string // Entity name
action: string // Action name
input: TInput // Validated input
resourceId?: string // ID from URL params
resource?: Record<string, unknown> // Fetched entity (if onResource)
user: User | null // Authenticated user
driver: Driver // Database driver
resolve: <T>(id: string) => T // Service container
resolveAsync: <T>(id: string) => Promise<T>
set: (key: string, value: any) => void // Workflow data
get: (key: string) => any // Workflow data
}Input Validation
Define input schema with field builders:
typescript
import { string, int, boolean } from "nevr"
checkout: action()
.input({
paymentMethodId: string,
quantity: int.min(1),
giftWrap: boolean.default(false),
})
.handler(async (ctx) => {
const { paymentMethodId, quantity, giftWrap } = ctx.input
// Input is validated and typed
})Authorization Rules
typescript
// Only authenticated users
publish: action()
.onResource()
.rules("authenticated")
.handler(...)
// Only the owner
delete: action()
.onResource()
.rules("authenticated", "owner")
.handler(...)
// Only admins
ban: action()
.onResource()
.rules("admin")
.handler(...)Using Services
Access registered services via ctx.resolve():
typescript
sendVerification: action()
.onResource()
.handler(async (ctx) => {
const mailer = ctx.resolve("mailer")
const token = ctx.resolve("crypto").generateToken()
await mailer.send({
to: ctx.resource.email,
subject: "Verify your email",
body: `Click here: /verify?token=${token}`,
})
return { sent: true }
})Pre-built Actions
Nevr provides common actions out of the box:
typescript
import {
softDeleteAction,
restoreAction,
archiveAction,
unarchiveAction,
cloneAction,
bulkUpdateAction,
bulkDeleteAction,
toggleAction,
exportAction,
countAction,
existsAction,
} from "nevr/plugins/core/actions"
const post = entity("post", {...}).actions({
softDelete: softDeleteAction(), // POST /:id/soft
restore: restoreAction(), // POST /:id/restore
archive: archiveAction(), // POST /:id/archive
clone: cloneAction(), // POST /:id/clone
bulkUpdate: bulkUpdateAction(), // PUT /bulk
bulkDelete: bulkDeleteAction(), // DELETE /bulk
togglePublished: toggleAction("published"), // POST /:id/toggle-published
export: exportAction(), // GET /export
count: countAction(), // GET /count
exists: existsAction(), // GET /:id/exists
})OpenAPI Metadata
Add metadata for documentation:
typescript
verify: action()
.onResource()
.meta({
summary: "Verify user email",
description: "Marks the user's email as verified after token validation",
tags: ["Authentication"],
})
.handler(...)Multiple Actions
typescript
const post = entity("post", {
title: string,
status: string.default("draft"),
}).actions({
publish: action()
.onResource()
.rules("owner")
.handler(async (ctx) => {
await ctx.driver.update("post", { id: ctx.resourceId }, { status: "published" })
return { published: true }
}),
unpublish: action()
.onResource()
.rules("owner")
.handler(async (ctx) => {
await ctx.driver.update("post", { id: ctx.resourceId }, { status: "draft" })
return { unpublished: true }
}),
duplicate: action()
.onResource()
.rules("owner")
.handler(async (ctx) => {
const { id, createdAt, updatedAt, ...data } = ctx.resource
const copy = await ctx.driver.create("post", { ...data, title: `${data.title} (Copy)` })
return copy
}),
})Next Steps
- Workflows - Multi-step actions with compensation
- Service Container - Dependency injection
- Authorization - Access control rules
