QUESTPIE
SchemaRelations

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.

View markdown

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

src/questpie/server/collections/comments.ts
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.

  • Relations for the single-target form and its query surface.
  • Blocks for content whose shape varies by entry, which is the other problem this looks like.

On this page