Text field
`f.text()` is the single-line string. The call picks the varchar width, the width becomes the Zod cap, and the field filters with the full string operator set.
| Call | Column | Derived schema |
|---|---|---|
f.text() | varchar(255) | z.string().max(255) |
f.text(n) | varchar(n) | z.string().max(n) |
f.text({ mode: "text" }) | text | z.string(), no cap |
Text mode is not a textarea. All three render the same single-line control, so
the argument moves the column and not the form. No method on the type reopens
the width. Only .array() and the .drizzle() escape hatch replace the column.
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" }), // text, no length cap
}));Without .required() the field selects as string | null. With it, string.
Methods
Six methods come with the type. .required(), .default(), .label(),
.localized() and the rest of the base chain work here as on any field.
| Method | Effect |
|---|---|
.min(n) | Adds z.string().min(n), a character count |
.max(n) | Adds z.string().max(n), a character count |
.pattern(re) | Adds z.string().regex(re) |
.trim() | Sets a trim flag |
.lowercase() | Sets a lowercase flag |
.uppercase() | Sets an uppercase flag |
username: f.text(32).required().min(3).pattern(/^[a-z0-9_]+$/),`.max()` tightens, it never widens
The constructor bakes its width into the schema and .max(n) adds a second
check beside it. f.text(255).max(500) still rejects 300 characters. Widen by
changing the constructor argument. Under { mode: "text" } there is no first
check, so .max(n) is the only cap.
`.trim()`, `.lowercase()` and `.uppercase()` do not touch the value
Each sets a flag. No code transforms the stored string, so .trim() writes " hi " unchanged. Their one reader is the CRDT eligibility check, which counts
them as refinements and rejects the field. Normalize with .zod() or a
beforeValidate hook, which runs before the schema.
Filtering
Text carries the stringOps set, fourteen operators.
| Operator | Operand | Matches |
|---|---|---|
eq / ne | string | Equal, not equal |
not | string, or null | Not equal, or IS NOT NULL when given null |
in / notIn | string[] | In the list, not in the list |
like / notLike | string | LIKE, case-sensitive, you write the % |
ilike / notIlike | string | The same, case-insensitive |
contains | string | Substring, the % are added for you |
startsWith | string | Prefix |
endsWith | string | Suffix |
isNull / isNotNull | boolean | Pass false and each inverts |
const { docs } = await app.collections.posts.find({
where: { title: { contains: "guide" }, slug: { eq: "my-first-post" } },
});contains, startsWith and endsWith build the pattern around a bound
parameter. The value is parameterized, but a % or _ inside it still reads as
a wildcard.
In the admin
The form control is TextField. The list cell is TextCell, which truncates
and puts the full value in a title attribute.
What crosses to the browser:
maxLengthbecomes the input'smaxLengthattribute, so typing stops at the width.{ mode: "text" }sends nomaxLengthuntil you call.max(n), so until then there is no attribute and no stop.- The admin rebuilds a Zod schema from
minLength,maxLengthandpattern, so the form checks before it posts. patterncrosses asRegExp.source, which drops the flags./^a/iis case-insensitive on the server and case-sensitive in the form.
A .max(n) above the constructor width is the number that crosses, so the
attribute and the browser schema both take it. The server schema keeps both
checks and the column keeps its width, so the form accepts what the write
rejects.
Lists
.array() replaces the column with jsonb and swaps the string operators for
the multi-value set. The per-item length still applies, and .minItems(n) /
.maxItems(n) bound the list.
tags: f.text(40).array().maxItems(10),Related
Textarea is the multi-line sibling, an unbounded
text column with the same operators.
Email and URL wrap a
varchar in a format check and extend stringOps with their own operators.
Arrays covers what .array() does to reads, writes
and filters.
Validation covers the derived schema and .zod(),
the way to normalize a value.
Eligible fields covers
why .crdt() takes f.text({ mode: "text" }) and never a varchar one.
One row per tenant
A scoped global keeps one row per city, property or customer behind a single definition, and picks the right one from the request context rather than from an argument you pass.
Textarea field
`f.textarea()` is the multi-line string. There is no argument to pass, the `text` column is unbounded whatever you write, and the field filters with the full string operator set.