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 instancesCommon Chain Methods
Every field supports these methods:
| Method | Description |
|---|---|
.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:
| Method | Description |
|---|---|
.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:
| Mode | PG Column | Use Case |
|---|---|---|
"integer" (default) | integer | Counts, IDs, quantities |
"smallint" | smallint | Small ranges (ports, flags) |
"bigint" | bigint | Large counts, timestamps |
"real" | real | Approximate decimals (4 bytes) |
"double" | double precision | Approximate decimals (8 bytes) |
{ mode: "decimal", precision?, scale? } | numeric(p,s) | Exact decimals (money) |
Number Chain Methods
| Method | Description |
|---|---|
.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:
| Option | Type | Default |
|---|---|---|
precision | 0-6 | 3 |
withTimezone | boolean | true |
f.time(config?)
Time of day. Default: precision 0, with seconds.
startTime: f.time(),
openAt: f.time({ precision: 3 }),Config:
| Option | Type | Default |
|---|---|---|
precision | 0-6 | 0 |
withSeconds | boolean | true |
Date/Time Chain Methods
| Method | Description |
|---|---|
.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
| Method | Description |
|---|---|
.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 jsonbRelation
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
| Method | Description |
|---|---|
.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:
| Option | Type | Default | Description |
|---|---|---|---|
to | string | "assets" | Target upload collection |
mimeTypes | string[] | — | Allowed MIME types |
maxSize | number | — | Max file size in bytes |
through | string | — | Junction collection for multi-upload |
sourceField | string | — | Source FK on junction |
targetField | string | — | Target 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
| Method | Description |
|---|---|
.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)
| Operator | Description |
|---|---|
equals | Exact match |
not_equals | Not equal |
in | In array |
not_in | Not in array |
contains | Case-insensitive substring |
starts_with | Starts with |
ends_with | Ends with |
is_empty | Is null or empty string |
Number Operators
| Operator | Description |
|---|---|
equals | Exact match |
not_equals | Not equal |
in | In array |
not_in | Not in array |
gt | Greater than |
gte | Greater than or equal |
lt | Less than |
lte | Less than or equal |
between | Between two values |
Boolean Operators
| Operator | Description |
|---|---|
equals | Exact match |
Date/Time Operators
| Operator | Description |
|---|---|
equals | Exact match |
not_equals | Not equal |
gt | After |
gte | On or after |
lt | Before |
lte | On or before |
between | Between two dates |
Select Operators
| Operator | Description |
|---|---|
equals | Exact match |
not_equals | Not equal |
in | In array of values |
not_in | Not in array |
Relation (belongs-to) Operators
| Operator | Description |
|---|---|
equals | Match by ID |
not_equals | Not equal to ID |
in | ID in array |
not_in | ID not in array |