One field, several targets
Pass a map of collections instead of a name and the field stores which collection it points at alongside the id.
A comment can hang off a blog post or off a page. One f.relation("posts")
cannot say that, so the field takes a map of the collections it is allowed to
point at.
Declaring it
import { collection } from "#questpie/factories";
export const comments = collection("comments").fields(({ f }) => ({
body: f.textarea().required(),
subject: f.relation({ posts: "posts", pages: "pages" }).required(),
}));Each key is the value stored in the discriminator, and each value names the
collection it stands for. The keys are checked against the same registry as the
values, so a discriminator has to be a collection name. The field becomes two
columns: subjectType, a varchar sized to the longest key with a floor of 50
characters, and subjectId, a varchar(36).
Reading and writing it
The two columns are an implementation detail. Both the read shape and the write shape are a single object.
await client.collections.comments.create({
body: "Booked, thanks.",
subject: { type: "posts", id: postId },
});
const comment = await client.collections.comments.findOne({ where: { id } });
// comment.subject → { type: "posts", id: "0d9a…" }Writing a type that is not one of the map's keys is a 400 Bad Request, as is
anything that is not a { type, id } pair. When both columns are null the field
reads back as null. .required() is enforced when the payload is validated,
and the two columns themselves stay nullable.
Fetching the row it names is a second call against whichever collection type
identifies. A polymorphic field has no single target, so with cannot resolve
it.
const subject =
comment.subject?.type === "posts"
? await client.collections.posts.findOne({
where: { id: comment.subject.id },
})
: await client.collections.pages.findOne({
where: { id: comment.subject.id },
});When to reach for it
Use the map when a field genuinely points at more than one collection and the set is open enough that a column per target would be worse. Two nullable belongsTo fields are easier to query and easier to constrain, so prefer them whenever the list of targets is short and stable.
Related
Loading related rows
The with clause decides which relations come back and what shape they arrive in. The options it accepts follow the relation's cardinality.
Rendering a page
BlockRenderer walks the saved tree and calls one of your components per node. QUESTPIE never renders your site, it hands you the values and gets out of the way.