QUESTPIE
Reference

Field API

Complete reference for all field factories and chain methods.

All fields are accessed through the f builder in .fields(({ f }) => {...}). Every chain method returns a new immutable field instance — you can safely reuse and extend base fields.

const base = f.text(255).required();
const title = base.label("Title");
const slug = base.label("Slug").inputOptional();
// base, title, and slug are all separate instances

Common Chain Methods

Every field supports these methods:

MethodDescription
.required()Mark as NOT NULL
.default(value)Set default value (makes input optional)
.label(text)Display label (string or { en: "...", sk: "..." })
.description(text)Help text (string or i18n object)
.localized()Store per-locale values in i18n table
.virtual(expr?)No DB column; optional SQL expression for computed fields
.array()Wrap as JSONB array of this field's type
.inputFalse()Exclude from API input (read-only)
.inputOptional()Always optional in input, even if NOT NULL (e.g. auto-generated slugs)
.outputFalse()Exclude from API output (write-only)
.hooks(h)Field-level lifecycle hooks
.access(a)Field-level access control
.operators(ops)Override the operator set for queries
.drizzle(fn)Escape hatch: modify the Drizzle column builder
.zod(fn)Escape hatch: modify the auto-derived Zod schema
.fromDb(fn)Transform value after reading from DB
.toDb(fn)Transform value before writing to DB
.minItems(n)Minimum items (for .array() fields)
.maxItems(n)Maximum items (for .array() fields)

Text Fields

f.text(maxLength?)

Short strings. Creates a varchar(maxLength) column (default 255).

title: f.text(255).required(),
slug: f.text(100).required().inputOptional(),

For unlimited-length text, pass a mode object:

bio: f.text({ mode: "text" }),

f.textarea()

Long text. Creates a PG text column (unlimited).

description: f.textarea().localized(),
notes: f.textarea(),

f.email(maxLength?)

Email addresses with built-in format validation. Default max length: 255.

email: f.email().required(),

f.url(maxLength?)

URLs with built-in format validation. Default max length: 2048.

website: f.url(),

Text Chain Methods

These methods are available on text, textarea, email, and url fields:

MethodDescription
.pattern(regex)Add regex validation
.trim()Trim whitespace
.lowercase()Lowercase transform
.uppercase()Uppercase transform
.min(n)Minimum string length
.max(n)Maximum string length
username: f.text(50).required().trim().lowercase().pattern(/^[a-z0-9_]+$/),

Number

f.number(mode?)

Numeric values. Default mode is "integer".

// Integer (default)
quantity: f.number().required(),

// Specific integer modes
port: f.number("smallint"),
totalViews: f.number("bigint"),

// Floating point
rating: f.number("real").min(0).max(5),
longitude: f.number("double"),

// Decimal (fixed precision)
price: f.number({ mode: "decimal", precision: 10, scale: 2 }).required(),

Modes:

ModePG ColumnUse Case
"integer" (default)integerCounts, IDs, quantities
"smallint"smallintSmall ranges (ports, flags)
"bigint"bigintLarge counts, timestamps
"real"realApproximate decimals (4 bytes)
"double"double precisionApproximate decimals (8 bytes)
{ mode: "decimal", precision?, scale? }numeric(p,s)Exact decimals (money)

Number Chain Methods

MethodDescription
.min(n)Minimum value
.max(n)Maximum value
.positive()Must be > 0
.int()Must be integer (useful for "real" / "double" modes)
.step(n)Step constraint (e.g., .step(0.01) for currency)
price: f.number({ mode: "decimal", precision: 10, scale: 2 }).positive().step(0.01),
rating: f.number().min(1).max(5),

Boolean

f.boolean()

isActive: f.boolean().default(true).required(),
isPublished: f.boolean().default(false),

No field-specific chain methods.

Date/Time Fields

f.date()

Calendar date (no time). PG date column, returns string ("2024-01-15").

publishedAt: f.date(),
birthDate: f.date().required(),

f.datetime(config?)

Timestamp with optional precision and timezone. Default: precision 3, with timezone.

scheduledAt: f.datetime().required(),
createdAt: f.datetime({ precision: 6 }).autoNow().inputFalse(),

Config:

OptionTypeDefault
precision0-63
withTimezonebooleantrue

f.time(config?)

Time of day. Default: precision 0, with seconds.

startTime: f.time(),
openAt: f.time({ precision: 3 }),

Config:

OptionTypeDefault
precision0-60
withSecondsbooleantrue

Date/Time Chain Methods

MethodDescription
.autoNow()Default to current date/time on create
.autoNowUpdate()Update to current date/time on every change
createdAt: f.datetime().autoNow().inputFalse(),
updatedAt: f.datetime().autoNowUpdate().inputFalse(),

Select

f.select(options)

Single value from a predefined list.

// Simple string options
status: f.select(["draft", "published", "archived"]).default("draft").required(),

// Options with labels (supports i18n)
status: f.select([
  { value: "pending", label: { en: "Pending", sk: "Cakajuce" } },
  { value: "confirmed", label: { en: "Confirmed", sk: "Potvrdene" } },
  { value: "completed", label: { en: "Completed", sk: "Dokoncene" } },
]).required().default("pending"),

Multi-Select

Use .array() to allow multiple selections:

tags: f.select(["frontend", "backend", "devops", "design"]).array(),

Select Chain Methods

MethodDescription
.enum(name)Use a PG enum type instead of varchar
status: f.select(["active", "inactive", "suspended"]).enum("user_status"),

Object

f.object(fields)

Nested structured data stored as JSONB. Pass fields directly — no wrapper function needed.

address: f.object({
  street: f.text(),
  city: f.text().required(),
  zip: f.text(10),
  country: f.text(2).required(),
}),

Nested objects compose naturally:

workingHours: f.object({
  monday: f.object({
    isOpen: f.boolean().default(true),
    start: f.time(),
    end: f.time(),
  }),
  tuesday: f.object({
    isOpen: f.boolean().default(true),
    start: f.time(),
    end: f.time(),
  }),
}),

Use helper functions to reduce repetition:

.fields(({ f }) => {
  const daySchedule = {
    isOpen: f.boolean().default(true),
    start: f.time(),
    end: f.time(),
  };

  return {
    workingHours: f.object({
      monday: f.object(daySchedule),
      tuesday: f.object(daySchedule),
      // ...
    }),
  };
})

Array of Objects

Use .array() on an object field:

socialLinks: f.object({
  platform: f.select(["instagram", "facebook", "twitter"]),
  url: f.url(),
}).array().maxItems(5),

JSON

f.json(config?)

Raw JSON data stored as JSONB. For structured data, prefer f.object().

metadata: f.json(),
config: f.json({ mode: "json" }),  // use PG json instead of jsonb

Relation

f.relation(target)

References to other collections. Creates a varchar(36) foreign key column.

// Simple belongs-to
author: f.relation("users"),
barber: f.relation("barbers").required(),

Has-Many

The FK lives on the target table. No column on this table — it's a virtual field.

posts: f.relation("posts").hasMany({ foreignKey: "authorId" }),

Many-to-Many

Uses a junction collection:

services: f.relation("services").manyToMany({
  through: "barberServices",
  sourceField: "barberId",
  targetField: "serviceId",
}),

Multiple (Inline Array)

Stores an array of FK UUIDs in a JSONB column:

assignees: f.relation("users").multiple(),

Polymorphic Relations

Pass a map of type → collection:

commentable: f.relation({
  post: "posts",
  page: "pages",
}),

Creates two columns: commentableType (varchar) and commentableId (varchar FK).

Relation Chain Methods

MethodDescription
.hasMany(config)One-to-many (FK on target table)
.manyToMany(config)Many-to-many via junction collection
.multiple()Store array of FKs inline as JSONB
.onDelete(action)Referential delete action
.onUpdate(action)Referential update action
.relationName(name)Disambiguate multiple relations to same target

Referential actions: "cascade", "set null", "set default", "restrict", "no action"

author: f.relation("users").required().onDelete("set null"),
category: f.relation("categories").onDelete("cascade"),

Upload

f.upload(config?)

File uploads linked to a storage collection.

// Simple upload (defaults to "assets" collection)
avatar: f.upload(),

// With constraints
avatar: f.upload({
  to: "assets",
  mimeTypes: ["image/*"],
  maxSize: 5_000_000,  // 5MB
}),

Multi-Upload (via junction)

gallery: f.upload({
  to: "assets",
  through: "productImages",
  mimeTypes: ["image/*"],
}),

Config:

OptionTypeDefaultDescription
tostring"assets"Target upload collection
mimeTypesstring[]Allowed MIME types
maxSizenumberMax file size in bytes
throughstringJunction collection for multi-upload
sourceFieldstringSource FK on junction
targetFieldstringTarget FK on junction

Custom

f.from(column, zodSchema?)

Escape hatch for custom Drizzle column types.

import { pgTable, citext } from "drizzle-orm/pg-core";

handle: f.from(citext, z.string().min(3).max(30)),

Custom Chain Methods

MethodDescription
.type(name)Override the type string used in introspection/metadata

Operators Reference

Each field type provides type-safe query operators for where clauses.

String Operators (text, textarea, email, url)

OperatorDescription
equalsExact match
not_equalsNot equal
inIn array
not_inNot in array
containsCase-insensitive substring
starts_withStarts with
ends_withEnds with
is_emptyIs null or empty string

Number Operators

OperatorDescription
equalsExact match
not_equalsNot equal
inIn array
not_inNot in array
gtGreater than
gteGreater than or equal
ltLess than
lteLess than or equal
betweenBetween two values

Boolean Operators

OperatorDescription
equalsExact match

Date/Time Operators

OperatorDescription
equalsExact match
not_equalsNot equal
gtAfter
gteOn or after
ltBefore
lteOn or before
betweenBetween two dates

Select Operators

OperatorDescription
equalsExact match
not_equalsNot equal
inIn array of values
not_inNot in array

Relation (belongs-to) Operators

OperatorDescription
equalsMatch by ID
not_equalsNot equal to ID
inID in array
not_inID not in array

On this page