Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fields

Fields define the schema of a collection or global. Each field maps to a SQLite column (except arrays and has-many relationships, which use join tables).

Defining Fields

There are two ways to define fields: factory functions (recommended) and plain tables.

crap.fields.* functions set the type automatically and return a plain table. Your editor shows only the properties relevant to each field type — no blocks on a text field, no options on a checkbox.

fields = {
    crap.fields.text({ name = "title", required = true }),
    crap.fields.select({ name = "status", options = {
        { label = "Draft", value = "draft" },
        { label = "Published", value = "published" },
    }}),
    crap.fields.relationship({ name = "author", relationship = { collection = "users" } }),
}

Plain Tables

You can also define fields as plain tables with an explicit type key. This is fully supported and equivalent — factories just set type for you.

fields = {
    { name = "title", type = "text", required = true },
    { name = "status", type = "select", options = { ... } },
}

Both syntaxes can be freely mixed in the same fields array.

Why factories? The types/crap.lua file ships per-type LuaLS classes (e.g., crap.SelectField, crap.ArrayField). When you use crap.fields.select({...}), your editor autocompletes only the properties that apply to select fields. With plain tables, the single crap.FieldDefinition class shows every possible property.

Common Properties

Every field type accepts these properties:

PropertyTypeDefaultDescription
namestringrequiredColumn name. Must be a valid SQL identifier (alphanumeric + underscore).
requiredbooleanfalseValidation: must have a non-empty value on create/update.
uniquebooleanfalseUnique constraint. Checked in the current transaction. For localized fields, enforced per locale.
indexbooleanfalseCreate a B-tree index on this column. Skipped when unique = true (already indexed by SQLite).
localizedbooleanfalseEnable per-locale values. Requires localization to be configured.
validatestringnilLua function ref for custom validation (see below).
default_valueanynilDefault value applied on create if no value provided.
admintable{}Admin UI display options.
hookstable{}Per-field lifecycle hooks.
accesstable{}Per-field access control.

Supported Types

TypeSQLite ColumnDescription
textTEXTSingle-line string (has_many for tag input)
numberREALInteger or float (has_many for tag input)
textareaTEXTMulti-line text
richtextTEXTRich text (HTML string)
selectTEXTSingle value from predefined options
radioTEXTSingle value from predefined options (radio button UI)
checkboxINTEGERBoolean (0 or 1)
dateTEXTDate/datetime/time/month with picker_appearance control
emailTEXTEmail address
jsonTEXTArbitrary JSON blob
codeTEXTCode string with syntax-highlighted editor
relationshipTEXT (has-one) or join table (has-many)Reference to one or more collections; supports polymorphic (collection = { "posts", "pages" })
arrayjoin tableRepeatable group of sub-fields
groupprefixed columnsVisual grouping of sub-fields (no extra table)
uploadTEXT (has-one) or join table (has-many)File reference to upload collection; supports has_many for multi-file
blocksjoin tableFlexible content blocks with different schemas
join(none)Virtual reverse relationship (read-only, computed at read time)

admin Properties

PropertyTypeDefaultDescription
labelstring | tablenilUI label (defaults to title-cased field name). Supports localized strings.
placeholderstring | tablenilInput placeholder text. Supports localized strings.
descriptionstring | tablenilHelp text displayed below the input. Supports localized strings.
hiddenbooleanfalseHide from admin UI forms
readonlybooleanfalseDisplay but don’t allow editing
widthstringnilField width: "full" (default), "half", or "third"
positionstring"main"Form layout position: "main" or "sidebar"
conditionstringnilLua function ref for conditional visibility (see Conditions)
stepstringnilStep attribute for number inputs (e.g., "1", "0.01", "any")
rowsintegernilVisible rows for textarea fields
collapsedbooleantrueDefault collapsed state for groups, collapsibles, array/block rows

Layout Wrappers

Row, Collapsible, and Tabs are layout wrappers — they exist only for admin UI grouping. They are transparent at the data layer: sub-fields are promoted as top-level columns with no prefix (unlike Group, which creates prefixed columns).

Nesting

Layout wrappers can be nested inside each other and inside Array/Blocks sub-fields at arbitrary depth:

crap.fields.array({
    name = "team_members",
    fields = {
        crap.fields.tabs({
            name = "member_tabs",
            tabs = {
                {
                    label = "Personal",
                    fields = {
                        crap.fields.row({
                            name = "name_row",
                            fields = {
                                crap.fields.text({ name = "first_name", required = true }),
                                crap.fields.text({ name = "last_name", required = true }),
                            },
                        }),
                        crap.fields.email({ name = "email" }),
                    },
                },
                {
                    label = "Professional",
                    fields = {
                        crap.fields.text({ name = "job_title" }),
                    },
                },
            },
        }),
    },
})

In this example, first_name, last_name, email, and job_title all become flat columns in the {collection}_team_members join table — the Tabs and Row wrappers are invisible at the data and API layer.

All combinations work: Row inside Tabs, Tabs inside Collapsible, Collapsible inside Row, etc.

Depth limit

The admin UI rendering caps layout nesting at 5 levels deep. Beyond this, fields are silently omitted from the form. This limit is a safety guard against infinite recursion — realistic schemas never hit it (5 levels means something like Array → Tabs → Collapsible → Row → Tabs → field).

The data layer (DDL, read, write, versions) has no depth limit.

Custom Validation

The validate property references a Lua function in module.function format. The function receives (value, context) and returns:

  • nil or true — valid
  • false — invalid with a generic message
  • string — invalid with a custom error message
-- hooks/validators.lua
local M = {}

function M.min_length_3(value, ctx)
    if type(value) == "string" and #value < 3 then
        return ctx.field_name .. " must be at least 3 characters"
    end
end

return M
-- In field definition:
crap.fields.text({ name = "title", validate = "hooks.validators.min_length_3" })

The context table contains:

FieldTypeDescription
collectionstringCollection slug
field_namestringName of the field being validated
datatableFull document data
usertable/nilAuthenticated user document (nil if unauthenticated)
ui_localestring/nilAdmin UI locale code (e.g., "en", "de")