hayes / pothos

Pothos GraphQL is library for creating GraphQL schemas in typescript using a strongly typed code first approach
https://pothos-graphql.dev
ISC License
2.28k stars 153 forks source link

Make possible to check type of a InputFieldRef during runtime #1209

Closed rolivegab closed 1 month ago

rolivegab commented 1 month ago

I'm creating an assist to help me writing graphql types without write a lot of things, so something like this:

builder.queryField("findOne", (t) =>
  t.field({
    type: itemObjectType,
    nullable: true,
    args: inputArgs(itemObjectType, t, {
      id: t.arg.string(),
    }),
    async resolve(_parent, args) {
      // const whereFragment = parseWhereInput(args.input?.where);
      // const item = await pool.maybeOne(
      //   sql.type(itemModel)`SELECT * FROM item${whereFragment}`
      // );
      // return item;
    },
  })
);

can turn into this:

image

the thing is that, while it's already working, I don't have a way to know if a t.arg.string() is a string during runtime, so I'm forced to write the args like this:

{
  args: inputArgs(itemObjectType, t, {
    id: ['string', t.arg.string()],
  }),
}

It would be much nicer if I have a way to tell, during runtime, that a t.arg.string() is from type 'string' without being forced to provide more information.

This is how implementation looks like internally:

objectInputWhereType.implement({
    fields: (t) => ({
      ...(R.fromEntries(
        R.toEntries(args).flatMap(([key, value]) => {
          if (Boolean("how to check that 'value' is a t.arg.string() here?")) {
            return [
              [
                key,
                t.field({
                  type: searchString(
                    `${objectRef.name}Input${toPascalCase(key)}`
                  ),
                }),
              ],
            ];
          }

          return [];
        })
      ) as InputFieldsFromShape<
        RecursivelyNormalizeNullableFields<Search<TArgs>>
      >),
      AND: t.field({
        type: [objectInputWhereType],
      }),
      OR: t.field({
        type: [objectInputWhereType],
      }),
    }),
  });
hayes commented 1 month ago

Using t.string in your helper is probably defining things backwards, and it looks like it might result in using arg fields where input object fields are expected.

The way I would build this is so that you define your helper more like:

args: inputArgs(itemObjectType, t, {
    id: 'string',
  }),

Then in your helper you can match on the input type to define the correct field type based on that

hayes commented 1 month ago

The slightly complicated part about that will be creating the right type, but if these are all scalar fields, you could use the Scalar type-names. This isn't exactly what you are looking for (needs some tweaking of arguments) but it shows how this could work:

builder.queryField('test', (t) =>
  t.field({
    type: 'String',
    args: getInputType(t, {
      foo: 'String',
    }),
    resolve: (_, args) => {
      console.log(args.AND?.foo);
      return 'test';
    },
  }),
);

type WhereInputShape<Types extends SchemaTypes, T extends Record<string, ScalarName<Types>>> = {
  [k in keyof T]: InputShape<Types, T[k]>;
} & {
  AND: WhereInputShape<Types, T> | null;
  OR: WhereInputShape<Types, T> | null;
};

function getInputType<
  Types extends SchemaTypes,
  T,
  Fields extends Record<string, ScalarName<Types>>,
>(
  fieldBuilder: PothosSchemaTypes.FieldBuilder<Types, T>,
  fields: Fields,
): {
  [k in keyof WhereInputShape<Types, Fields>]: InputFieldRef<WhereInputShape<Types, Fields>[k]>;
} {
  const WhereInput = builder.inputRef<WhereInputShape<Types, Fields>>(
    `WhereInput${fieldBuilder.typename}`,
  );

  WhereInput.implement({
    fields: (t) =>
      ({
        ...Object.fromEntries(
          Object.entries(fields).map(([key, value]) => [key, t.field({ type: value as never })]),
        ),
        AND: t.field({ type: [WhereInput] }),
        OR: t.field({ type: [WhereInput] }),
      }) as any,
  });

  return {
    ...Object.fromEntries(
      Object.entries(fields).map(([key, value]) => [
        key,
        fieldBuilder.arg({ type: value as never }),
      ]),
    ),
    AND: fieldBuilder.arg({ type: [WhereInput] }),
    OR: fieldBuilder.arg({ type: [WhereInput] }),
  } as any;
}
rolivegab commented 1 month ago

Thank you @hayes ! Your idea worked very well and I learned a lot from it.