Type variants
How to define multiple GraphQL types based on the same drizzle table
A variant gives another GraphQL representation to the same database row. The publishing API uses public User fields for author pages and a private Viewer for the signed-in account.
The API has three parts:
variantregisters an additional type instead of naming the primary type withname.t.variantreturns another representation of the same row.- A relation's
typeoption can select a variant using its returned object ref.
A single object variant
Use variant to define Viewer alongside the primary User type. t.variant('users') exposes
the public representation of the same row:
const Viewer = builder.drizzleObject('users', {
variant: 'Viewer',
select: {},
fields: (t) => {
return {
id: t.exposeID('id'),
user: t.variant('users'),
drafts: t.relation('posts', {
query: {
where: { published: false },
orderBy: { createdAt: 'desc', id: 'desc' },
},
}),
};
},
});A viewer with role-specific fields
When viewer fields differ by role, Viewer can instead be an interface with object variants for each role. The following definition uses this structure; Interfaces explains its implementations:
export const Viewer = builder.drizzleInterface('users', {
variant: 'Viewer',
select: { columns: { id: true, role: true } },
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
fields: (t) => ({
user: t.variant('users'),
email: t.exposeString('email'),
drafts: t.relation('posts', {
query: { where: { published: false }, orderBy: { id: 'asc' } },
}),
}),
});The root lookup uses the authenticated context ID. It does not accept an arbitrary author's ID:
me: t.drizzleField({
type: Viewer,
nullable: true,
resolve: (query, _root, _args, ctx) => {
return db.query.users.findFirst(
query({
where: { id: ctx.userId },
}),
);
},
}),Maya (userId: 1) sees “Planning the spring exchange”; Leo (userId: 2) sees “Saving rainwater.”
The interface and variants describe the shapes; the root lookup restricts ownership. Public
author fields and post connections return published posts only.
Conditionally expose another variant
The publishing User also exposes its Viewer conditionally. Define that field after Viewer is registered, which avoids a circular reference between their definitions:
builder.drizzleObjectField('users', 'viewer', (t) =>
t.variant(Viewer, {
select: { columns: { id: true } },
isNull: (user, _args, ctx) => user.id !== ctx.userId,
}),
);The ownership check is essential: without it an arbitrary author's public row could reveal that
author's drafts. isNull makes the variant field null when its parent is another account.
A variant field accepts a select just like other fields. Here it loads id for the ownership
check; the target variant contributes the selections required by its own fields.
Relation variants
To use a different representation of a related row, pass its object ref to t.relation's type
option. The variant must represent the relation's target table. For example, this alternative
adds a compact PostSummary representation and selects it for a public author's posts:
const PostSummary = builder.drizzleObject('posts', {
variant: 'PostSummary',
fields: (t) => ({ title: t.exposeString('title') }),
});
builder.drizzleObjectField('users', 'postSummaries', (t) =>
t.relation('posts', {
type: PostSummary,
query: {
where: { published: true },
orderBy: { id: 'asc' },
},
}),
);Two variants selected for the same row have their type-level selections merged. Conflicting relation arguments or extras can fail; see Conflicting selections between variants.