Customise Tensei resources with fields
A field represents a database column.
Each Tensei resource has a fields
method. This method takes in an array of fields that define the columns on the table this resource maps to.
Fields are much more than just columns. They can represent any way of collecting data from the user. Tensei ships with a variety of fields out of the box, including fields for text inputs, booleans, dates, file uploads, Markdown, Wysiwyg editors, Rich editors and more.
To create a field, first you need to identify the type of data this field would be storing. Let's take a Post
resource for example. A Post might have a Title
field and a Published
field. The title field would be a varchar
column, and the published
field would be a boolean
column.
If you're using a NoSQL database, the title would be a String
field, and published would be a Boolean
field.
import { tensei, resource, text, boolean } from '@tensei/core'
export default tensei()
.resources([
resource('Post')
.fields([
text('Title'),
boolean('Published')
])
])
Tensei will "snake case" the name of the field to determine the underlying database column. For example, the text('Example Title')
field would connect to the example_title
column. However, if necessary, you may pass the column name as the second argument to the field's method:
text('Title', 'custom_title_column')
As mentioned above, the defined fields would not only be used to determine the database columns, but also the types of input components to display on the dashboard.
Often, you will only want to display a field in certain situations. For example, there is typically no need to show a Password
field on a resource index listing. Likewise, you may wish to only display a Created At
field on the creation / update forms. Tensei makes it a breeze to hide / show fields on certain screens.
The following methods may be used to show / hide fields based on the display context:
showOnIndex
showOnDetail
showOnCreate
showOnUpdate
hideOnIndex
hideOnDetail
hideOnCreate
hideOnUpdate
onlyOnForms
exceptOnForms
You may chain any of these methods onto your field's definition in order to instruct Tensei where the field should be displayed:
resource('Post')
.fields([
text('Title')
.hideOnIndex()
.hideOnUpdate()
])
The Title field would be hidden from the resource index table because we called hideOnIndex()
, and will be hidden when updating a Post
because we called hideOnUpdate()
.
Sometimes you want to completely prevent a field from being exposed to the API. You may use a combination of these methods to control where fields show up in the API:
hideFromCreateApi()
hideFromUpdateApi()
hideFromDeleteApi()
hideFromFetchApi()
hideFromShowApi()
Let's say you want to prevent a secret token field from being exposed to all data fetching endpoints, but be exposed to the create and update endpoints:
text('Secret')
.hideFromFetchApi()
.hideFromShowApi()
You may also use the hideFromApi()
method to completely hide a field from showing up in any calls to the API.
When attaching a field to a resource, you may use the sortable
method to indicate that the resource index may be sorted by the given field:
resource('Post')
.fields([
text('Title')
.sortable()
])
You may use the searchable
method to indicate that this field can be searched. When the search box on the resource index is used, only the searchable fields would be queried. All searchable fields would also be indexed in the database.
resource('Post')
.fields([
text('Title')
.searchable()
])
This portion of the documentation only discusses non-relationship fields. To learn more about relationship fields, check out their documentation.
Tensei ships with a variety of field types. So, let's explore all of the available types and their options:
The Boolean
field may be used to represent a boolean / "tiny integer" column in your database. For example, assuming your database has a boolean column named active
, you may attach a Boolean
field to your resource like so:
import { resource, boolean } from '@tensei/core'
resource('Customer')
.fields([
boolean('Active')
])
A Boolean
field would show up as a checkbox on dashboard forms.
The Date
field may be used to store a date value (without time).
import { resource, date } from '@tensei/core'
resource('Employee')
.fields([
date('Birthday')
])
You can default this field to the current time using .defaultToNow()
method. If a default value is not defined when creating a resource, this value would default to the current date.
date('Birthday')
.defaultToNow()
You may customize the display format of your Date
fields using the format
method. This format won't affect how the date is saved to the database. The format must be a format supported by Day.js:
date('Birthday')
.format('do MMM yyyy, hh:mm a'),
The DateTime
field may be used to store a date-time value. It will also show a date/time picker on dashboard forms.
import { resource, dateTime } from '@tensei/core'
resource('Employee')
.fields([
dateTime('Started On')
])
You may customize the display format of your DateTime
fields using the format
method. This format won't affect how the date is saved to the database. The format must be a format supported by Day.js:
dateTime('Started On')
.format('do MMM yyyy, hh:mm a'),
The Timestamp
field may be used to store a timestamp value. It will also show a date/time picker on dashboard forms.
import { resource, timestamp } from '@tensei/core'
resource('Employee')
.fields([
timestamp('Last Logged In At')
])
Just like the date and dateTime fields, you can format timestamps for the dashboard using the .format()
method.
The Integer
field provides an input
control with a type
attribute of number
:
import { resource, integer } from '@tensei/core'
resource('Book')
.fields([
integer('Price')
])
You may use the min
, max
, and step
methods to set their corresponding attributes on the generated input
control. This also applies to other number types such as bigInteger
, float
, and double
.
integer('Price').min(1).max(1000).step(0.01)
The bigInteger
field provides an input
control with a type
attribute of number
. It will also store the data in a bigInt
database column. It defaults to a number column if the database being used does not support bigint.
import { resource, bigInteger } from '@tensei/core'
resource('Book')
.fields([
bigInteger('Price')
])
The double
field provides an input
control with a type
attribute of number
. It will also store the data in a double
database column.
import { resource, double } from '@tensei/core'
resource('Book')
.fields([
double('Price')
])
The float
field provides an input
control with a type
attribute of number
. It will also store the data in a float
database column.
import { resource, float } from '@tensei/core'
resource('Book')
.fields([
float('Price')
])
The Select
field may be used to generate a drop-down select menu. The select menu's options may be defined using the options
method:
import { select } from '@tensei/core'
resource('Book')
.fields([
select('Category')
.options([{
label: 'Postgresql',
value: 'pg'
}, {
label: 'SQL database',
value: 'sql'
}])
])
The Text
field provides an input
control with a type
attribute of text
:
import { text } from '@tensei/core'
resource('Book')
.fields([
text('Title')
])
Text fields may be customized further by setting any attribute on the field. This can be done by calling the htmlAttributes
method:
text('Title')
.htmlAttributes({
placeholder: 'Enter your email'
})
The Slug
field provides a read-only input control that automatically generates a url-friendly slug based on another field.
slug('Slug')
.from('Title')
.type('date')
The above slug would be automatically generated from()
the Title
field. As you type the title on the CMS, you should see this change reflect.
You may call the .editable()
method if you do not want to slug to be read-only. You'll be able to update it to whatever you need.
The Textarea
field provides a textarea
control:
import { textarea } from '@tensei/core'
resource('Book')
.fields([
textarea('Description')
])
By default, Textarea fields will not display their content when viewing a resource on its detail page. It will be hidden behind a "Show Content" link, that when clicked will reveal the content. You may specify the Textarea field should always display its content by calling the alwaysShow
method on the field itself:
textarea('Description').alwaysShow()
You may also specify the textarea's height by calling the rows
method on the field:
textarea('Description').rows(7)
Textarea fields may be customized further by setting any attribute on the field. This can be done by calling the htmlAttributes
method:
textarea('Description').htmlAttributes({
placeholder: 'Some description of this book ...'
})
If you want to add a not nullable constraint on a column, use the .notNullable()
method:
text('Title').notNullable()
If you want the opposite, you may call the .nullable()
method to make a field nullable.
Sometimes you want to add a unique constraint to a database field. You may do so by calling the .unique()
method on the field:
text('Email')
.unique()
To set a database default value for a field, you may use the .default()
method.
bigInt('Views')
.default(1000)
If you want to set the default to a database function such as current_timestamp
, use the .defaultRaw()
method instead:
timestamp('Expires At')
.defaultRaw('current_timestamp(3)')
If you would like to place "help" text beneath a field on the create / update form, you may use the description
method. This description will also show up on API documentation.
text('Description').description('Provide a clear description of this book.')
You can modify a field before it is inserted into the database. An example use case is hashing a password. You can pass a callback to the .onCreate()
method that returns the modified value of the field.
text('Password')
.onCreate((user) => Bcrypt.hashSync(user.password))
You can modify a field value before an update query is made to the database. An example use case is an updated_at
timestamp. We may want this to update to the current date everytime the database row is updated.
field('Updated At')
.onUpdate((user) => new Date())