Selections

Selecting the columns and relations your fields need

Selections describe the database data a GraphQL field needs. The example User starts with select: {} so requesting one field does not load unrelated columns or a profile.

Field selections

An exposed column, such as t.exposeString('firstName'), contributes its own selection. A custom resolver must declare the columns it reads:

fullName: t.string({
  select: { columns: { firstName: true, lastName: true } },
  resolve: (user) => `${user.firstName} ${user.lastName}`,
}),

Only load a profile when requested

A biography is a string in GraphQL, but it comes from the optional profile relation. The field selects that relation only when requested:

bio: t.string({
  nullable: true,
  select: { with: { profile: true } },
  resolve: (user) => user.profile?.bio,
}),

A missing profile returns null. A name-only query does not load the profile; adding bio adds the relation to the database query.

Computed SQL values

The same mechanism supports SQL expressions through extras. For example, a field can select lowercaseName only when the GraphQL operation requests it:

lowercaseName: t.string({
  select: {
    extras: {
      lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`,
    },
  },
  resolve: (user) => user.lowercaseName,
}),

Type selections

By default, a drizzleObject gives its resolvers access to all columns of the table. A type-level select replaces that default and makes its selected data available to every field resolver. This alternative always loads the name, profile, and expression:

const User = builder.drizzleObject('users', {
  name: 'User',
  select: {
    columns: {
      firstName: true,
      lastName: true,
    },
    with: {
      profile: true,
    },
    extras: {
      lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`
    },
  },
  fields: (t) => {
    return {
      fullName: t.string({
        resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
      }),
      bio: t.string({
        nullable: true,
        resolve: (user) => user.profile?.bio,
      }),
      lowercaseName: t.string({
        resolve: (user) => user.lowercaseName,
      }),
    };
  },
});

Any selections added to the type will be available to consume in all resolvers. Columns that are not selected can still be exposed as before.

Use type selections for data required whenever the type is loaded, and field selections for data needed by a particular field. Both are merged into the parent query; they do not make each field an independent database query. See Query planning.