QUESTPIE
SchemaFields

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.

View markdown
CallColumnDerived schema
f.text()varchar(255)z.string().max(255)
f.text(n)varchar(n)z.string().max(n)
f.text({ mode: "text" })textz.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.

src/questpie/server/collections/posts.ts
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.

MethodEffect
.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.

OperatorOperandMatches
eq / nestringEqual, not equal
notstring, or nullNot equal, or IS NOT NULL when given null
in / notInstring[]In the list, not in the list
like / notLikestringLIKE, case-sensitive, you write the %
ilike / notIlikestringThe same, case-insensitive
containsstringSubstring, the % are added for you
startsWithstringPrefix
endsWithstringSuffix
isNull / isNotNullbooleanPass 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:

  • maxLength becomes the input's maxLength attribute, so typing stops at the width. { mode: "text" } sends no maxLength until you call .max(n), so until then there is no attribute and no stop.
  • The admin rebuilds a Zod schema from minLength, maxLength and pattern, so the form checks before it posts.
  • pattern crosses as RegExp.source, which drops the flags. /^a/i is 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),

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.

On this page