params
Requests to your server will often contain either a request body, path params, or query params. Psychic controllers provide special helper methods for interacting with these params. They enable the developer to both keep DRY, as well as providing implicit param validation.
castParam
The castParam method provides the developer with a unique way to both absorb and validate a specific param simultaneously. In the following example, the show action will find any places belonging to the currentHost with the id provided in the params. Psychic is param-location ambiguous, meaning that it will search the path, query, and body for a param with the key id, and will then cast it to a string type. However, if the param passed is not a valid string, Psychic will raise a 400 status code error.
import { OpenAPI } from '@rvoh/psychic'
import Place from '../../../models/Place.js'
import V1HostBaseController from './BaseController.js'
const openApiTags = ['places']
export default class V1HostPlacesController extends V1HostBaseController {
@OpenAPI(Place, {
status: 200,
tags: openApiTags,
description: 'Fetch a Place',
})
public async show() {
const place = await this.place()
this.ok(place)
}
private async place() {
return await this.currentHost.associationQuery('places').findOrFail(this.castParam('id', 'string'))
}
}
Psychic supports many different data types for validation. Here are a few others that can be used:
export default class V1HostPlacesController extends V1HostBaseController {
public async show() {
const uuid = this.castParam('id', 'uuid')
const int = this.castParam('id', 'integer')
const bigint = this.castParam('id', 'bigint')
const date = this.castParam('id', 'date')
const datetime = this.castParam('id', 'datetime')
const number = this.castParam('id', 'number')
const string = this.castParam('id', 'string')
const enum = this.castParam('id', 'string', { enum: ['basic', 'premium']})
}
}
In addition to these primitive types, Psychic also supports array variants:
export default class V1HostPlacesController extends V1HostBaseController {
public async show() {
const uuids = this.castParam('id', 'uuid[]')
const ints = this.castParam('id', 'integer[]')
const bigints = this.castParam('id', 'bigint[]')
const dates = this.castParam('id', 'date[]')
const datetimes = this.castParam('id', 'datetime[]')
const numbers = this.castParam('id', 'number[]')
const strings = this.castParam('id', 'string[]')
const enums = this.castParam('id', 'string[]', {
enum: ['basic', 'premium'],
})
}
}
casting openapi shapes
The castParam method is also able to interpret OpenAPI shapes, and will return types data back to you based on the provided shape.
const data = this.castParam('myData', {
type: 'object',
properties: {
a: number,
b: { oneOf: [{ type: 'string' }, { type: 'number' }] },
},
})
data.a // number | undefined
data.b // string | number | undefined
When using castParam with OpenAPI shapes, params will automatically be coerced by the Ajv library.
extractParams
In addition to individual param casting, Psychic provides the ability to ingest a typed, allowlisted set of params for a given Dream model. extractParams takes the model class and an explicit list of permitted columns; the columns are visible at the call site so reviewers can see exactly which fields the action accepts from the request:
export default class V1HostPlacesController extends V1HostBaseController {
@OpenAPI(Place, {
status: 201,
tags: openApiTags,
description: 'Create a Place',
})
public async create() {
let place = await this.currentHost.createAssociation(
'places',
this.extractParams(Place, ['name', 'description', 'style', 'sleeps']),
)
if (place.isPersisted) place = await place.loadFor('default').execute()
this.created(place)
}
}
The allowed array is compile-time constrained to the model's param-safe column names, so passing a protected column (primary key, timestamps, deletedAt, STI type, belongs-to foreign keys, polymorphic type fields, or any column declared in paramUnsafeColumns) is a TypeScript error. At runtime, the array is intersected against the model's paramSafeColumnsOrFallback() set, so anything that bypasses the type system still fails closed.
extractParams is the recommended primitive for controllers handling user-editable models. The generator emits it for non-admin namespaces.
extractImplicitParams
When the model's declared paramSafeColumns is the canonical allowlist and duplicating it at every call site would create drift, use extractImplicitParams. It pulls the allowlist from the model itself:
export default class AdminBalloonsController extends AdminBaseController {
public async create() {
const balloon = await Balloon.create(this.extractImplicitParams(Balloon))
this.created(balloon)
}
}
When the model does not declare paramSafeColumns, extractImplicitParams falls back to the framework's default-safe set (every column except the protected ones listed above).
The generator emits extractImplicitParams for admin-namespace scaffolds. Either default can be overridden at scaffold time with --with-extract-params or --with-extract-implicit-params.
paramSafeColumns
The attributes that may be updated via params can be further restricted by providing a paramSafeColumns getter on any Dream model (see paramSafeColumns):
export default class User extends ApplicationModel {
public get paramSafeColumns(): DreamColumnNames<User>[] {
return ['email'] as const
}
}
extractImplicitParams(User) returns only the declared columns; extractParams(User, ['email']) continues to require the explicit allowlist and intersects it with the declared safe columns.
Options
Both methods accept an optional second/third argument:
// Extract from a nested key (e.g. `{ place: { name: '...' } }`)
this.extractParams(Place, ['name'], { key: 'place' })
// Extract an array of param objects
this.extractParams(Place, ['name'], { array: true })