Text field
f.text() is a single-line string field backed by a Postgres varchar (or unbounded text), with length, pattern, and string-operator filtering built in.
f.text() stores a single-line string. By default it produces a varchar(255) column, derives a Zod schema that enforces the length, and exposes the full set of string filter operators (eq, contains, ilike, …) on the typed where clause. Pass a length to size the column, or switch to an unbounded text column when you need it.
Prerequisites: read Fields first, this page covers only the
texttype. Thefproxy, the chain-modifier model, and shared modifiers like.required()/.localized()/.default()are taught there.
What it does
- Stores a single-line string, default
varchar(255); pass a number to resize, or{ mode: "text" }for an unbounded Postgrestextcolumn. - Validates length, the column length becomes a Zod
.max()automatically; add.min(n)/.max(n)for explicit bounds and.pattern(re)for a format check. - Filters with string operators,
eq,ne,in,like,ilike,contains,startsWith,endsWith,isNull, and more, all typed againststring. - Renders a text input in the admin form (a multiline editor is what
f.textarea()gives you instead).
Quick start
Use f.text() inside a .fields() callback. The positional argument is the max character length; chain modifiers to refine it.
import { collection } from "#questpie/factories";
export const posts = collection("posts").fields(({ f }) => ({
title: f.text().required(), // varchar(255), NOT NULL
slug: f.text(120).required(), // varchar(120)
summary: f.text({ mode: "text" }), // unbounded `text` column
}));That gives posts a title column typed string on read, required on insert, validated at ≤ 255 chars, and filterable with the string operators below, no schema or migration written by hand.
Constructor argument
f.text() has two call signatures.
| Call | Column | Notes |
|---|---|---|
f.text() | varchar(255) | Default. maxLength defaults to 255. |
f.text(n) | varchar(n) | Sized varchar; n is also applied as a Zod .max(n). |
f.text({ mode: "text" }) | text | Unbounded Postgres text. No length cap on the schema. |
The resulting field data is always string. The derived Zod schema is z.string(), with a .max() added whenever a length is known (the 255 default or your n); { mode: "text" } drops the cap.
Chained methods
These methods are specific to text (on top of the shared modifiers every field has). Each returns a new immutable field, so they chain in any order.
| Method | Effect |
|---|---|
.min(n) | Minimum string length (Zod .min(n)). |
.max(n) | Maximum string length (Zod .max(n)). |
.pattern(re) | Validate against a RegExp (Zod .regex(re)). |
.trim() | Marks the field as trimmed, see the gotcha below. |
.lowercase() | Marks the field as lowercased, see the gotcha below. |
.uppercase() | Marks the field as uppercased, see the gotcha below. |
collection("users").fields(({ f }) => ({
username: f
.text(32)
.required()
.min(3) // length 3..32
.pattern(/^[a-z0-9_]+$/), // lowercase letters, digits, underscore
}));`.min()` / `.max()` here are STRING LENGTH, not value bounds
On a text field, .min(n) / .max(n) constrain the number of characters (they map to minLength / maxLength). On f.number() the same method names constrain the numeric value. Same names, different meaning, sized by the field's type.
`.trim()` / `.lowercase()` / `.uppercase()` are no-ops today
These three methods set runtime state but are not wired into the Zod schema. applyRefinements only applies maxLength, minLength, and pattern for string types, so .trim() / .lowercase() / .uppercase() neither transform nor validate the stored value. If you need the value normalized, do it explicitly with .zod() (e.g. f.text().zod((s) => s.trim().toLowerCase())) or a beforeChange hook.
Filtering, operators
text uses the string operator set (stringOps), so every text field is filterable with these in a where clause. The operand is typed string (or string[] for in / notIn).
| Operator | Operand | Matches |
|---|---|---|
eq / ne | string | Exact equal / not equal. |
in / notIn | string[] | Value is (not) in the list. |
like / notLike | string | SQL LIKE (case-sensitive, your own %). |
ilike / notIlike | string | Case-insensitive LIKE. |
contains | string | Substring (LIKE '%value%'). |
startsWith | string | Prefix (LIKE 'value%'). |
endsWith | string | Suffix (LIKE '%value'). |
isNull / isNotNull | boolean | Column is (not) null. |
// Case-insensitive title search + an exact-slug lookup
const { docs } = await app.collections.posts.find({
where: { title: { contains: "guide" }, slug: { eq: "my-first-post" } },
});These operators are field-level filters; the full query language, combining them with AND / OR / NOT, pagination, and orderBy, is covered in the querying reference (page forthcoming).
When to use it
f.text(), short, single-line strings: titles, slugs, names, SKUs, labels. Defaultvarchar(255)is the right size for most of these.f.text({ mode: "text" }), long single-value strings with no natural length cap (a raw description, a serialized token) that still render as a plain input.- For multi-line content with a textarea editor in the admin, reach for
f.textarea(). For formatted content, usef.richText().
Multiple values
Chain .array() to store a list of strings in a single jsonb column, and bound it with .minItems(n) / .maxItems(n). This is a shared modifier, not text-specific:
tags: f.text(40).array().maxItems(10), // string[] stored as jsonbTypeScript
A text field contributes a string to the generated row, insert, and where types, no annotation needed. After questpie generate, pull the shapes off the collection or the app types:
type Post = typeof posts.$infer.select;
// ^? { id: string; title: string; slug: string; summary: string | null; ... }.required() makes the field non-null and required on insert; without it the column is nullable and the insert field optional. See Fields → inferred types for how modifiers flow into the generated types.
Related
- Fields, the
fproxy, the chain-modifier model, and the shared modifiers (.required(),.localized(),.default(),.array(), …). - Textarea field, the multi-line sibling.
- Validation, the auto-derived Zod schema and the
.zod()escape hatch (the way to actually normalize a value).