Cauen / prisma-generator-pothos-codegen

The fastest way to create a fully customizable CRUD Graphql API from Prisma Schema.
https://www.npmjs.com/package/prisma-generator-pothos-codegen
95 stars 16 forks source link

Augmenting generated `data` input arg #27

Closed coopbri closed 1 year ago

coopbri commented 1 year ago

Hey, thank you so much for this library, this is exactly what I was trying to do with Pothos + Prisma and the key feature I was missing from Nexus. I have a question about augmenting the generated data argument in generated input types.

I am able to add a custom arg like so:

builder.mutationFields((t) => {
    const field = ...;

    return {
        fieldName: t.prismaField({
            ...field,
            args: {
                ...field.args,
                customArg: t.arg({ ... });
            },
        });
    }
});

That works great. However, I would like to augment the data arg with that field. Is there an easy way to do this? For example, instead of the input being

data: { ... }, customArg: ...

I'd like to have the custom arg stored inside of data like:

data: {
    ...,
    customArg: ...
}

I tried adjusting the data arg a couple ways:

Cauen commented 1 year ago

Hi @coopbri. Thanks for using and your words, I'm really glad to see it helping others.

Once that every input type in Graphql must be defined, you cant freely update like objects, you need to define a new...

1. First approach:

You can create a input type with a property linked to the original input, and set extra freely. ✔ Keeps the expressiveness of the args ✔ Auto sync with database ❌ But breaks the original shape of the data arg...

export const UserUpdateInput = builder.inputRef<{
  original: Prisma.UserUpdateInput,
  customArg: string;
}>('UserUpdateInputCustom').implement({
  fields: (t) => ({
    original: t.field({ "required": false, "type": Inputs.UserUpdateInput }),
    customArg: t.field({ "required": true, "type": "String" }), // Custom
  }),
});

export default builder.mutationFields((t) => {
  const field = updateOneUserMutationObject(t);
  return {
    updateOneUserCustom: t.prismaField({
      ...field,
      args: {
        ...field.args,
        data: t.arg({ type: UserUpdateInput, required: true }),
      },
      resolve: async (...args) => {
        const [include, root, { data }, { response, db }, info] = args;
        if (data.customArg === "123") throw new Error("Invalid");
        return field.resolve(...args);
      },
    }),
  };
});

2. Second approach:

If you need the data.arg original shape (ie: for a auto generated admin panel) i think the easiest way its copy original input definition and set the custom fields. Neither pothos nor this lib also offer a function to easily create derived args. So it won't automatically stay in sync with the database... @saphewilliam any idea on this?

✔ Keeps the expressiveness of the args ❌ Auto sync with database ✔ Original shape of the data arg...

export const UserUpdateInput = builder.inputRef<Prisma.UserUpdateInput & { customArg?: string }>('UserUpdateInputCustom').implement({
  fields: (t) => ({
    email: t.field({ "required": false, "type": Inputs.StringFieldUpdateOperationsInput }),
    name: t.field({ "required": false, "type": Inputs.NullableStringFieldUpdateOperationsInput }),
    Address: t.field({ "required": false, "type": Inputs.AddressNullableUpdateEnvelopeInput }),
    Posts: t.field({ "required": false, "type": Inputs.PostUpdateManyWithoutAuthorNestedInput }),
    customArg: t.field({ "required": false, "type": "String" }), // Custom
  }),
});

...
args: {
  ...field.args,
  data: t.arg({ type: UserUpdateInput, required: true }),
},
...

Do any of the approaches helps you?

saphewilliam commented 1 year ago

Great use case! I hadn't considered it before.

I think I would go for option 2, creating a custom UserUpdateInput and supplying that to your resolver. Since this custom arg does not come from the Prisma schema, we cannot auto-generate it in any way, but we could make it as easy as possible to create these custom input types from the auto generated ones.

I can't check this right now, but does something along these lines work?

import { UserCreateInput } from './inputs.ts';

const CustomUserCreateInput = builder.inputRef<Prisma.UserUpdateInput & { customArg?: string }>('CustomUserCreateInput').implement({
  fields: (t) => ({
    ...UserCreateInput.fields(t)
    customArg: t.field({ "required": false, "type": "String" }), // Custom
  }),
});

If this works, we should probably add this use case to the docs. If it doesn't work, we should probably figure out a way to make it work to support this use case.

Cauen commented 1 year ago

@saphewilliam This doesn't work

I think we could export 2 variables for each input, like this:

// __generated__/inputs.ts
export const UserUpdateInputFields = (t: any) => ({
  email: t.field({ "required": false, "type": Inputs.StringFieldUpdateOperationsInput }),
  name: t.field({ "required": false, "type": Inputs.NullableStringFieldUpdateOperationsInput }),
  Address: t.field({ "required": false, "type": Inputs.AddressNullableUpdateEnvelopeInput }),
  Posts: t.field({ "required": false, "type": Inputs.PostUpdateManyWithoutAuthorNestedInput }),
})
export const UserUpdateInput = builder.inputRef<Prisma.UserUpdateInput>('UserUpdateInputCustom3').implement({
  fields: (t) => UserUpdateInputFields(t),
});

// User/inputs.ts
export const UserUpdateInputCustom = builder.inputRef<Prisma.UserUpdateInput & { customArg: string }>('UserUpdateInputCustom').implement({
  fields: (t) => ({
    ...UserUpdateInputFields(t),
    customArg: t.field({ "required": true, "type": "String" }), // custom
  }),
});

Even with (t: any), its typesafe because types are linked to the input itself, but i dont know if im confortable with it XD What do you think?

Prints ![image](https://user-images.githubusercontent.com/8796757/214333859-c6b2053b-e914-4cf6-961c-31622bc61dfa.png) ![image](https://user-images.githubusercontent.com/8796757/214337561-b59f5250-c87c-4ed8-9e94-b7861f4f58f6.png)
saphewilliam commented 1 year ago

Yeah this would be a great solution imo! We could put any there for now and we'll see later if we can do something more fancy if that's even necessary.

Cauen commented 1 year ago

@coopbri We update with a new feature to augment inputs. Version: 0.5.3 version

Docs

Please update and try :D

coopbri commented 1 year ago

@Cauen @saphewilliam you guys are amazing! I was planning on helping implement a solution but you knocked it out super fast. I just tested 0.5.3 and it works great. All I did was:

  1. Update to 0.5.3
  2. Regenerate types
  3. Saw all of the new input field artifacts were generated (as expected)
  4. Augmented and implemented composed custom input fields based on the pattern above and in the docs

...and it worked like a charm. I really appreciate your time and effort, seriously. This solution is super clean, consistent with the rest of the API, and works flawlessly.