-
-
Notifications
You must be signed in to change notification settings - Fork 164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Idea: input types with built-in transform #1290
Comments
We probably won't be able to add this to pothos/core, but something similar should be achievable through a plugin. There are existing mechanism in pothos that allow plugins to transform inputs. The API would likely be something like I have been intending to implement a new validation plugin that works this way (the biggest drawback of the zod plugin was the lack of transforms). The goal would be to support arbitrary validations + transforms without being tied to a specific validation library. Implementing a plugin that just handles transformations would be relatively easy. I can try to find some time this weekend to put together a quick prototype that you can use to add that as a custom plugin |
that sounds good, a plugin would be good enough |
Here's a basic transform plugin you can use/modify to fit your use-case import { createServer } from 'node:http';
import type { PothosOutputFieldConfig, SchemaTypes } from '@pothos/core';
import SchemaBuilder, {
BasePlugin,
createInputValueMapper,
InputObjectRef,
mapInputFields,
resolveInputTypeConfig,
} from '@pothos/core';
import { createYoga } from 'graphql-yoga';
declare global {
namespace PothosSchemaTypes {
export interface InputObjectRef<Types extends SchemaTypes, T> {
transform: <U>(fn: (value: T) => U) => InputObjectRef<Types, U>;
}
export interface Plugins<Types extends SchemaTypes> {
transformInputs: TransformInputsPlugin<Types>;
}
}
}
InputObjectRef.prototype.transform = function (
this: InputObjectRef<SchemaTypes, unknown>,
fn: (value: unknown) => unknown,
) {
this.updateConfig((config) => {
return {
...config,
extensions: {
...config.extensions,
inputTransformFunctions: [
...((config.extensions?.inputTransformFunctions as []) ?? []),
fn,
],
},
};
});
return this;
};
export class TransformInputsPlugin<Types extends SchemaTypes> extends BasePlugin<Types> {
override onOutputFieldConfig(
fieldConfig: PothosOutputFieldConfig<Types>,
): PothosOutputFieldConfig<Types> | null {
const argMappings = mapInputFields(fieldConfig.args, this.buildCache, (inputField) => {
let inputTypeConfig: ReturnType<typeof resolveInputTypeConfig> | null;
try {
inputTypeConfig = resolveInputTypeConfig(inputField.type, this.buildCache);
} catch (_) {
inputTypeConfig = null;
}
if (inputTypeConfig?.extensions?.inputTransformFunctions) {
return (input: unknown) =>
input == null
? input
: (
inputTypeConfig.extensions?.inputTransformFunctions as ((
value: unknown,
) => unknown)[]
).reduce<unknown>((acc, fn) => fn(acc), input);
}
return null;
});
if (!argMappings) {
return fieldConfig;
}
const argMapper = createInputValueMapper(argMappings, (value, mapping) =>
mapping.value ? mapping.value(value) : value,
);
return {
...fieldConfig,
argMappers: [...(fieldConfig.argMappers ?? []), (args) => argMapper(args)],
};
}
}
SchemaBuilder.registerPlugin('transformInputs', TransformInputsPlugin);
const builder = new SchemaBuilder<{}>({
plugins: ['transformInputs'],
});
const TransformedInput = builder
.inputType('ToTransform', {
fields: (t) => ({
value: t.string({ required: true }),
}),
})
.transform(({ value }) => ({
number: Number.parseInt(value, 10),
}));
builder.queryType({
fields: (t) => ({
test: t.int({
args: {
input: t.arg({
required: true,
type: TransformedInput,
}),
inputList: t.arg({
required: true,
type: [TransformedInput],
}),
},
resolve: (_, args) => {
console.log(args);
return args.inputList.reduce((a, b) => a + b.number, args.input.number);
},
}),
}),
});
const yoga = createYoga({
schema: builder.toSchema(),
maskedErrors: false,
});
const server = createServer(yoga);
const port = 3000;
server.listen(port); |
thank you very much, i'll try that out |
@hayes i tried it, it works for sync transforms. unfortunatly, it does not work with async function, or at least, not directly. i tried to see whether I can fix this in the plugin, but it looks like this is deep down in pothos core where the mapper is called and i did not see whether you can resolve the promise somewhere there |
I would be very hesitant to add a sync input transforms into Pothos. What kinds of transforms are you doing that require async calls? The argument mappers are used in paces that can't easily be made async. You can definitely handle async mapping in a custom plugin, but you would need to use the wrapResolver hook, and not use the built-in arg mapper. |
the transform require and additional db request in my case. Since you mentioned validation as use case, I am pretty sure a common request will be for async validation 😉 isn't the mapper also using the wrapResolver? |
It does internally, but the built in mapping won't handle anything async, so you would need to use wrapResolver directly I think if you are making db calls, input transforms are probably the wrong place to be doing that. I would recommend just abstracting into into a re-usable helper. Async validation is also a contentious topic. It hasn't been supported in pothos, and I don't think I've seen any requests for it before now. This would be another place where I'd probably leave it up to being handled inside the resolver if you need to do async work |
i wouldn't necessary think its wrong to do input transforms with the help of some async function and technically there is nothing that I see that should prevent it (outside from the refactoring effort to put async/await in the right spot). I understand that you can't make everything async. But in this case, I think it should be completly doable. i understand input transform as some kind of "middleware", same as any wrapResolver. Its maybe a bit funky though, if the input type has code that does db or any ai call though, but It would really be extremly helpful. What alternatives do i have otherwise? (apart from just not abstracting it at all) |
Sometimes you want to create multiple queries and mutations that all use the same input object and want to pass that to your orm, like prisma:
however, this requires that the backing type of
ProjectWhereUniqeInputRef
has the same shape as prisma requires as were.If its not the same, you have to transform it, but you have to do that on each query:
this is of course doable, but can be a bit awkward.
An elegant solution would be that
ProjectWhereUniqeInputRef
would already do this transformation:so wherever you use
ProjectWhereUniqueInputRef
in an argument, the object has theTransformedInputShape
in the resolver instead ofInputShape
The text was updated successfully, but these errors were encountered: