With-Input plugin
Define fields whose arguments live in a single generated input object with t.fieldWithInput.
GraphQL convention is to give a mutation one argument (an input object) rather than a loose list of scalars. Writing that out by hand means declaring a separate input type for every field. The with-input plugin collapses the two steps: t.fieldWithInput takes the input fields inline, generates the input object type for you, and wires it up as the field's argument.
npm install --save @pothos/plugin-with-inputAdd the plugin, then define input fields with the t.input builder. Pothos names and registers the input type on first use.
import SchemaBuilder from '@pothos/core';
import WithInputPlugin from '@pothos/plugin-with-input';
const builder = new SchemaBuilder({
plugins: [WithInputPlugin],
});
const Team = builder.objectRef<ITeam>('Team').implement({
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
wins: t.exposeInt('wins'),
}),
});
builder.queryType({
fields: (t) => ({
team: t.fieldWithInput({
type: Team,
nullable: true,
input: {
id: t.input.id({ required: true }),
},
resolve: (_root, args) => Teams.get(Number(args.input.id)) ?? null,
}),
}),
});The input object maps field names to t.input.* definitions, the same scalar builders as t.arg, scoped to the generated input type. The resolver reads them off args.input. The generated schema:
type Query {
team(input: QueryTeamInput!): Team
}
input QueryTeamInput {
id: ID!
}Why a generated input
A single input argument is the standard shape for anything that mutates or takes structured arguments: new fields can be added without touching the call site, and large argument lists stay out of the field signature. t.fieldWithInput saves you from declaring a separate input type for each field, and keeps the input's definition next to the field it serves.
Multiple input fields
Every entry under input becomes a field on the generated type, so a mutation with several arguments still needs exactly one type declaration:
builder.mutationType({
fields: (t) => ({
renameTeam: t.fieldWithInput({
type: Team,
nullable: true,
input: {
id: t.input.id({ required: true }),
name: t.input.string({ required: true }),
},
resolve: (_root, args) => {
const team = Teams.get(Number(args.input.id));
if (!team) {
return null;
}
team.name = args.input.name;
return team;
},
}),
}),
});This generates input MutationRenameTeamInput { id: ID!, name: String! }. You can still declare ordinary arguments alongside the input by passing an args option; they sit next to input on the field rather than inside the generated type.
Naming the input type and argument
By default the input type name is ${ParentType}${FieldName}Input (QueryTeamInput, MutationRenameTeamInput) and the argument is called input. Override either per field with typeOptions.name and argOptions.name. Renaming the argument also renames the key you read in the resolver:
builder.mutationType({
fields: (t) => ({
createTeam: t.fieldWithInput({
type: Team,
typeOptions: { name: 'NewTeamInput' },
argOptions: { name: 'team' },
input: {
name: t.input.string({ required: true }),
},
resolve: (_root, args) => {
const team: ITeam = { id: nextId++, name: args.team.name, wins: 0 };
Teams.set(team.id, team);
return team;
},
}),
}),
});Both option bags forward the rest of their keys to the underlying type and argument, so typeOptions accepts a description, argOptions accepts deprecationReason, and so on.
To change the default naming scheme for the whole schema instead of one field, pass a name callback in withInput.typeOptions. It receives the parent type and field name and returns the input type name:
const builder = new SchemaBuilder({
plugins: [WithInputPlugin],
withInput: {
typeOptions: {
name: ({ parentTypeName, fieldName }) => {
const capitalized = `${fieldName[0].toUpperCase()}${fieldName.slice(1)}`;
// Drop the Query/Mutation prefix from root fields.
if (parentTypeName === 'Query' || parentTypeName === 'Mutation') {
return `${capitalized}Input`;
}
return `${parentTypeName}${capitalized}Input`;
},
},
},
});Optional inputs
The input argument is required by default. Set argOptions.required: false to make the whole argument optional, as for a search or filter field that can run with no input at all:
builder.queryType({
fields: (t) => ({
searchTeams: t.fieldWithInput({
type: [Team],
argOptions: { required: false },
input: {
namePrefix: t.input.string({ required: true }),
},
resolve: (_root, args) => {
const prefix = args.input?.namePrefix;
const teams = [...Teams.values()];
return prefix ? teams.filter((team) => team.name.startsWith(prefix)) : teams;
},
}),
}),
});When the argument is optional, args.input is nullable; read it with optional chaining (args.input?.namePrefix). The individual t.input.* fields keep their own required flags independently of the argument.
To flip the default for the whole schema, set withInput.argOptions.required on the builder and declare the matching WithInputArgRequired in your SchemaTypes so the resolver argument types line up:
const builder = new SchemaBuilder<{ WithInputArgRequired: false }>({
plugins: [WithInputPlugin],
withInput: {
argOptions: {
required: false,
},
},
});Schema-wide defaults
withInput.typeOptions and withInput.argOptions on the builder set defaults for every generated input type and argument. Per-field typeOptions/argOptions merge over them. This is the place for cross-cutting choices: a default description on generated inputs, or the required default above:
const builder = new SchemaBuilder({
plugins: [WithInputPlugin],
withInput: {
typeOptions: {
// Applied to every input type this plugin generates.
},
argOptions: {
// Applied to every generated input argument.
},
},
});Prisma integration
With the Prisma plugin installed, t.prismaFieldWithInput combines the generated input with a Prisma-backed field, so the resolver receives the query selection alongside args:
builder.queryField('user', (t) =>
t.prismaFieldWithInput({
type: 'User',
nullable: true,
input: {
id: t.input.id({ required: true }),
},
resolve: (query, _root, args) =>
prisma.user.findUnique({
where: { id: Number.parseInt(args.input.id, 10) },
...query,
}),
}),
);Options reference
t.fieldWithInput takes every option a normal field takes, plus:
| Option | Purpose |
|---|---|
input | Map of field names to t.input.* definitions. Becomes the generated input type. |
typeOptions | Options for the generated input type. name overrides the type name; other keys (description, …) forward to the input object. |
argOptions | Options for the input argument. name renames the argument (and the resolver key); required toggles nullability. |
args | Ordinary arguments declared alongside input, outside the generated type. |
Builder-level withInput:
| Option | Purpose |
|---|---|
withInput.typeOptions | Defaults for every generated input type. name accepts a ({ parentTypeName, fieldName }) => string callback to customize the naming scheme. |
withInput.argOptions | Defaults for every generated input argument, including required. |
WithInputArgRequired | SchemaTypes flag that sets whether input arguments are required by default. Pair it with withInput.argOptions.required. |