Select field
f.select() takes a list of options, or a handler that produces one. A static list narrows the read type to the literal union of its values and derives a z.enum from them.
| Call | Column | Read type | Schema |
|---|---|---|---|
f.select([{ value, label }, …]) | varchar(n), n is the longest value, min 50 | union of the values | z.enum(values) |
f.select([]) | varchar(255) | string | z.string() |
f.select({ handler, deps }) | varchar(255) | string | z.string() |
.enum(name) on a static list | the Postgres enum type name | unchanged | unchanged |
.array() | jsonb | a list of the same value | z.array(…) |
import { collection } from "#questpie/factories";
export const posts = collection("posts").fields(({ f }) => ({
status: f
.select([
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
])
.required()
.default("draft"),
}));posts.status reads back as "draft" | "published", and .default("archived")
does not compile. Drop the .required() and the read type picks up null. The
column is varchar(50), because 50 is the floor and no value is longer.
The option object
value and label are required. The other four shape the admin, and QUESTPIE
serializes all six into the introspection payload.
| Key | Type | What reads it |
|---|---|---|
value | string | number | The stored value. A number is written as its string |
label | I18nText | The dropdown row and the list cell |
description | I18nText | A second line under the label in the dropdown |
disabled | boolean | The dropdown row, which stops responding |
icon | ComponentReference | Leads the dropdown row and the list cell |
className | string | Classes on the dropdown row and on the list cell |
I18nText is a plain string, a locale map such as { en: "Ready" }, or a
{ key } reference your i18n adapter resolves. Icons come from the c proxy
that arrives beside f.
import { collection } from "#questpie/factories";
export const deployments = collection("deployments").fields(({ f, c }) => ({
health: f.select([
{
value: "ready",
label: { en: "Ready", sk: "Pripravené" },
description: "Runtime is healthy",
icon: c.icon("ph:check-circle"),
className: "border-emerald-500/30 bg-emerald-500/10",
},
{ value: "degraded", label: "Degraded", icon: c.icon("ph:warning") },
]),
}));The union needs the values in the call
Narrowing comes from a const type parameter, so the literals have to be
visible where you call f.select(). Inline options narrow. A hoisted array
needs as const or the read type falls back to string.
.enum(name)
The one method select adds. It builds a fresh pgEnum(name, values) from the
static values and swaps the column factory to it. Nothing else moves, not the
read type, not the derived schema, not the operators.
status: f
.select([
{ value: "draft", label: "Draft" },
{ value: "published", label: "Published" },
])
.enum("post_status"),`.enum()` can end up declaring nothing
On f.select([]) or a handler there are no values, so the call is a silent
no-op and the column stays varchar(255). Even on a static list,
app.getSchema() hands questpie push the tables only, and the pgEnum
stays closed over inside the column factory. Read the generated SQL.
Filtering
A select carries the single-value operator set. The typed where map is exactly
these six.
| Operator | Operand | SQL |
|---|---|---|
eq / ne | one value | = / <> |
in / notIn | a list of values | IN / NOT IN |
isNull / isNotNull | boolean | IS NULL / IS NOT NULL, inverted by false |
const { docs } = await app.collections.posts.find({
where: { status: { ne: "draft" } },
});.array()
.array() is the shared modifier, not something select defines. On a select it
turns the column into jsonb, wraps the schema as z.array(z.enum(values)), and
takes .minItems() and .maxItems(). The admin renders one dropdown per row.
tags: f
.select([
{ value: "news", label: "News" },
{ value: "guide", label: "Guide" },
])
.array()
.maxItems(5),An arrayed select keeps the single-value `where`
.array() swaps the runtime set for selectMultiOps, but the select carries
its own whereInput through the wrapping. So the typed map stays eq / ne
/ in / notIn on one value, eq runs against the whole array, and the
membership operators are unreachable from the typed clause.
A handler instead of a list
Pass { handler, deps } when the choices are not known at build time. The field
then stores a plain string, and introspection ships an empty option list plus
the form paths to watch.
| Key | Type | Default |
|---|---|---|
handler | (ctx) => OptionsResult | Promise<…> | Required |
deps | string[] or (ctx) => any[] | None, the watch list is empty |
Name the fields you read. Introspection tracks a deps function, never the
handler, so omitting deps ships an empty watch list and nothing refetches as
the form changes.
The handler receives { data, sibling, search, page, limit, ctx }. data is the
current form, page counts from 0, limit defaults to 20 and caps at 100, and
ctx is { db, user, locale } plus an optional req, with db typed
unknown. Return { options, hasMore?, total? }. Each option is a value and a
label, and that pair is all that crosses the wire.
city: f.select({
handler: async ({ data }) =>
data.country === "us"
? { options: [{ value: "nyc", label: "New York" }] }
: { options: [] },
deps: ["country"],
}),Fetching them takes one hook
The admin serves these options over its own route, and useFieldOptions from
@questpie/admin/client calls it with the form data, search and page. Wire
that hook into your own field component. The built-in control renders the
options it was handed, which for a handler is none.
In the admin
The form renders a dropdown, with a search box once the list passes eight
options. The list cell renders a badge carrying the option's icon, label and
className. Four keys sit on .admin() for a select, on top of the ones every
field shares. Only the first reaches the control.
| Key | Type | Effect |
|---|---|---|
clearable | boolean, default true | The clear button, and Backspace or Delete |
displayAs | "dropdown" | "radio" | "checkbox" | "buttons" | Accepted by the type, read by nothing |
searchable | boolean | Accepted by the type, read by nothing |
creatable | boolean | Accepted by the type, read by nothing |
Related
Array is the modifier itself, on every type rather than this one.
Relations is what you want when the choices are rows in another table, with a real foreign key.
Validation covers the derived schema and .zod().
Reading and writing covers the rest of the query language around the operators above.