QUESTPIE
ConceptsFields

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 text type. The f proxy, 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 Postgres text column.
  • 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 against string.
  • 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.

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" }),   // 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.

CallColumnNotes
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" })textUnbounded 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.

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

OperatorOperandMatches
eq / nestringExact equal / not equal.
in / notInstring[]Value is (not) in the list.
like / notLikestringSQL LIKE (case-sensitive, your own %).
ilike / notIlikestringCase-insensitive LIKE.
containsstringSubstring (LIKE '%value%').
startsWithstringPrefix (LIKE 'value%').
endsWithstringSuffix (LIKE '%value').
isNull / isNotNullbooleanColumn 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. Default varchar(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, use f.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 jsonb

TypeScript

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.

  • Fields, the f proxy, 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).

On this page