Time field
`f.time()` is a clock reading with no date and no time zone. Its two config keys look like a pair and move entirely separate things.
Signature
| Call | Column | Accepted input |
|---|---|---|
f.time() | time(0) | HH:MM or HH:MM:SS(.s…) |
f.time({ precision: n }) | time(n), n is 0 to 6 | unchanged |
f.time({ withSeconds: false }) | time(0) | HH:MM only |
The column is time without time zone. QUESTPIE never hands Drizzle a
withTimezone option, so the zoned variant is out of reach.
A time value is a string in TypeScript, in JSON and through the typed client.
Nothing converts it to a Date at any boundary. None of the instant handling
around f.datetime() reaches it.
import { collection } from "#questpie/factories";
export const businesses = collection("businesses").fields(({ f }) => ({
name: f.text().required(),
opensAt: f.time().required(), // time(0), NOT NULL
closesAt: f.time().required(),
lastCall: f.time({ withSeconds: false }), // nullable, HH:MM only
}));What the schema accepts
The derived schema is z.string().regex(…). The config picks which regex.
withSeconds | Accepts | Rejects |
|---|---|---|
true, the default | 09:30, 09:30:00, 09:30:00.5 | 9:30, 24:00, 09:60 |
false | 09:30 | 09:30:00, 9:30, 24:00 |
Both patterns want two digits for the hour, so 09:30 passes and 9:30 fails.
Hours run 00 to 23, minutes and seconds 00 to 59. Rejection reads
Invalid time format. and then the shape it wanted. The default pattern asks
for Expected HH:MM or HH:MM:SS. The strict one asks for Expected HH:MM.
`withSeconds: true` is the loose setting
The name reads like a demand for seconds. It makes them optional. false is
the strict one, HH:MM and nothing longer. The admin's time input emits
minute precision, so a seconds-required default once made time fields
unsavable.
`precision` and `withSeconds` never meet
precision reaches the Postgres column and stops there. withSeconds reaches
the regex and stops there. So { precision: 3 } stores milliseconds while the
schema still takes 09:30. And { withSeconds: false } leaves the column at
time(0).
Methods
The type adds none of its own. Its fieldType() declaration carries no methods
bag. You get the shared chain and nothing more: .required(), .default(),
.label(), .localized(), .array(), .zod() and the rest on
Fields.
.default("09:00") type-checks against string. It lands on the Postgres
column as a column default too. Nothing runs that literal past the regex first.
No `.autoNow()` here
.autoNow() and .autoNowUpdate() belong to the date and datetime factories.
A bare clock reading has no framework notion of now. Write the literal you
want with .default(), or compute one in a beforeChange field hook.
Filtering
A time field carries dateOps. f.datetime() uses the same set. f.date()
does not. It carries dateStringOps, the same eleven operators with a string
operand. So a time field's where entry types to exactly these keys.
| Operator | Operand | Matches |
|---|---|---|
eq, ne | Date | string | Equal, not equal |
not | Date | string | null | Not equal, or IS NOT NULL when you pass null |
gt, gte | Date | string | Later, at or later |
lt, lte | Date | string | Earlier, at or earlier |
in, notIn | (Date | string)[] | In the list, not in the list |
isNull, isNotNull | boolean | Null check, inverted when you pass false |
A bare value types alongside that object. { opensAt: "09:00" } compiles to
eq, and { opensAt: null } to IS NULL.
const { docs } = await app.collections.businesses.find({
where: { opensAt: { lte: "12:00" }, closesAt: { gt: "18:00" } },
});Pass the string, not a `Date`
The operand types as Date | string only because the set is shared. The where
builder normalizes datetime and date operands and names no other type. A
time operand reaches Postgres exactly as you wrote it. Give it the string you
store.
In the admin
The form control is TimeField. It wraps a native <input type="time"> with a
clock icon and a clear button that writes null. The list cell is TimeCell.
It prints the stored string in tabular figures, and a - for null. Its column
is 180 pixels wide by default. The filter builder gives the field its own
<input type="time"> and the date operator list.
Neither config key crosses to that control.
precisiondoes not. The control carries its ownprecision,"minute"or"second". Nothing derives it from the field, so the step stays at 60 seconds.f.time({ precision: 3 })widens the column only. What moves the step is.admin({ precision: "second" }). That key is undeclared, and the untyped signature below lets it through.- The pattern does not.
f.time()records nomaxLength,pattern,minormax, so nothing carries the configured regex across.
The browser checks a different pattern
The admin prefers the server's JSON Schema. Without one it falls back to a Zod
schema. That schema's time case hardcodes its own regex. The regex takes a
one-digit hour, rejects fractional seconds and ignores withSeconds. Trust
the server message over the form's.
.admin(config) sets the rest of the control. It appears on the field once
@questpie/admin is enabled. Its generated signature takes unknown, so
nothing type-checks the keys you pass.
| Key | Type | Effect |
|---|---|---|
placeholder | string | Placeholder text on the input |
That one key is the whole declared list. showInList, listWidth, sortable,
filterable, hidden, readOnly, colspan and the rest come from the base
admin config every field type shares.
Lists
.array() replaces the column with jsonb. It replaces the operator set too.
The four ordering keys go, and so do ne, not, in and notIn. eq
survives but now takes the whole list. .minItems(n) and .maxItems(n) bound
the list.
slots: f.time().array().maxItems(24),Array items get a text box, not a picker
The admin allowlists text, number, email, textarea and select as
array item types. time is missing from it, so each row falls back to a plain
text input. The server still checks every item against the regex.
Arrays has the operator table that replaces this one.
Types
A time field contributes string to the row and the insert. Its where operand
widens to Date | string, inherited from the shared date operator set.
.required() makes it non-null and mandatory on insert. Without it the column
is nullable and the value selects as string | null. .default(…) makes the
input optional.
type Business = typeof businesses.$infer.select;
// ^? { id: string; opensAt: string; lastCall: string | null; ... }import type { CollectionDoc, CollectionWhere } from "#questpie";
type Business = CollectionDoc<"businesses">;
type BusinessFilter = CollectionWhere<"businesses">;Related
Temporal values is the wire contract for
all three temporal types. Its tables carry the time row too. That row is a
time(0) column, the same string in and out, and no temporal format in the
OpenAPI schema.
f.datetime() is the instant. It stores a
Date in a timestamptz column.
f.date() is the calendar day, an exact
YYYY-MM-DD string. It owns .autoNow(), and so does f.datetime().
Arrays covers what .array() does to reads, writes
and filters.
Validation covers the derived schema and .zod().
Reading and writing covers the rest of where.
Datetime field
`f.datetime()` stores one instant. The column is a Postgres `timestamp`, every read hands back a JS `Date`, and the only string it accepts carries a zone.
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.