graphql-nexus / nexus

Code-First, Type-Safe, GraphQL Schema Construction
https://nexusjs.org
MIT License
3.4k stars 275 forks source link

Field level resolver makes the field disappear from TS typegen #1006

Closed halotest closed 2 years ago

halotest commented 3 years ago

Hi. I want to store 1-many relation in an array which contains ids to related objects. orders: [123, 456, 789] And then I want that field level resolver to grab related objects but as soon as i add resolver function the field disappears and TS raises an error saying field does not exist on type. Relevant image:

Снимок экрана 2021-10-29 в 11 26 24

For example having only type: type: list("Order") does not remove the field from TS typegen but as soon as resolve func is added, the field is gone.

What's the problem here? Am i misunderstanding something?

jcpage commented 3 years ago

I just noticed this exact issue. As soon as I define a resolver on a field it disappears from the root type in the NexusGenObjects interface, so I can't access the field from the source parameter in the resolve function.

I see this code intentionally skips writing it in that case... but I'm not sure why? Is it because not having a resolver is the only way to guarantee the source will have a field of he same name, since it doesn't know about the real data sources? (and is this something that the nexus-prisma plugin, for example, would be able to address?)

https://github.com/graphql-nexus/nexus/blob/c0e55b53b45dfdc4a7e8aae69d1ed685389331e1/src/typegenPrinter.ts#L595

halotest commented 3 years ago

@jcpage so i think i figured how to get around the problem but i'm not sure about the reasoning behind such complexity nor do i understand it completely. According to this doc , fields you define in your nexus objects aren't 1-1 mapping to your database fields(why?). You need to define your interfaces elsewhere and let nexus map them to it's own objects for exact field matching(why?). If you look up the screenshot of my issue above, the solution that i found is that i defined an interface for this object like this export interface IProduct { orders: Order[] | string[] } and suddenly TS error is gone and the field is there in nexus typegen. So since i'm also using graphql-codegen i just mapped its types to nexus source types and we gucci(why?).

But i'm still confused why do we need to use a separate tool to generate another bunch of types and give them to nexus on top of its own types. what's the point then? Another thing i don't understand why fields we define in nexus aren't 1-1 map to what we get from database? The example with fullName from docs is weird because if client needs fullName we should just be able to define it as a separate field and resolve as name+lastname without writing additional interfaces or using other tools like ORM's or codegenerators to interface. Also in my own interface above i defined just one field and that just worked. My resolvers didn't complain about other fields i didn't define, so, apparently, nexus merges your own defined interfaces with its own from object definitions. Just let us define field level resolve to be something like this type: list("Obj") | list("String") without using additional tools.

Why so much magic behind the scenes? Nexus generates schema then i have to generate types with gqlcodegen (or ORM) from the schema(which nexus already generates) and use types FROM gql codegen in my resolvers because otherwise nexus will complain that fields i defined resolvers for don't end up in nexus codegen. So i don't see how nexus solves the issue it claims to solve(typescript) if i'm forced to use my own types?

Need answers or better documentation.

tgriesser commented 3 years ago

Yeah, this has been a longstanding source of confusion. The original intention of Nexus is that you're providing a way to resolve your "source types" globally: https://nexusjs.org/docs/guides/source-types#globally-configure-source-types

I use a fork of schemats for this. If source / parent is an object that's an ORM class, then you'd want a way to know it's that object and not a vanilla JS object. I use a codegen of the database with a fork of schemats for this, but for simpler types or if you don't want/need to do this, it should be possible to manage this on the object's field definition.

I'm thinking maybe we add a convenience helper for this, sourceType?

sourceType: true - preserve sourceType: 'string | number', etc - specify custom typing

t.list.field('orders', {
  type: 'Order',
  sourceType: true,
  resolve: () => // ...
})

If true it will preserve whatever the typing would otherwise be for the field based on the definition, so in this case:

parent.orders // Order[] | null | undefined

@jasonkuhrt thoughts?

jcpage commented 3 years ago

Thanks @tgriesser, I think I understand the intent better now. For the the original issue your helper might cover it - in my case I think I just misunderstood the issue I was having, which is that I need to better define my source types externally when graphql doesn't match the db.

Just throwing out an idea - it might interesting if you were able to add something to help infer source fields in the same definition... like another function that only defined it for the source (naming convention totally made up and could be improved, but hopefully you get my idea). Basically the feature that this would implement is automatically including fields in the generated source type that you DON'T want in the generated graphql schema:

t.sourceField('imageKey', {
  type: 'String'
})
t.string('imageUrl', {
  resolve: (src) => { return `https://cdn.path/${src.imageKey}` }
})

Or maybe you could have an option that basically excluded it from the graphql schema so it's only in the source type (this may be somewhat confusing since the point of nexus is generating schema so skipping that part seems odd... though it could potentially work with your suggestion 'sourceType' so you effectively can tell it to include it in the schema, the source type, or both)

t.string('imageKey', {
  sourceOnly: true
})
t.string('imageUrl', {
  resolve: (src) => { return `https://cdn.path/${src.imageKey}` }
})

I suppose this could easily be done with a plugin (especially the first suggestion) - maybe I'll play around with it ;)

jcpage commented 3 years ago

Re: my previous comment - from the start I was surprised Nexus couldn't just use the Prisma types that were generated and specified in sourceTypes in makeSchema. I found if I changed it to explicitly reference the generated ts instead of the module it works (by just defining the source type to be "MyType: prisma.MyType")

        //module: '@prisma/client',
       module: '.prisma/client/index.d.ts',

I guess the previous suggestion might still be useful if people don't want to use Prisma types or aren't using Prisma at all...

nenadfilipovic commented 3 years ago

@jcpage this is enough module: '.prisma/client',

tgriesser commented 3 years ago

For the the original issue your helper might cover it - in my case I think I just misunderstood the issue I was having, which is that I need to better define my source types externally when graphql doesn't match the db.

Yeah, the recommended way to do that inline currently is by configuring the sourceType for the type:

export const User = objectType({
  name: 'User',
  definition(t) {
    t.string('imageUrl', {
     resolve: (src) => { return `https://cdn.path/${src.imageKey}` }
    })
  },
  sourceType: '{ imageKey: string }'
})
export interface UserShape {
  imageKey: string
}

export const User = objectType({
  name: 'User',
  definition(t) {
    t.string('imageUrl', {
     resolve: (src) => { return `https://cdn.path/${src.imageKey}` }
    })
  },
  sourceType: {
    module: __filename,
    export: 'UserShape'
  }
})
halotest commented 3 years ago

Another problem i'm facing regarding this topic and looking for helps is, i have a global interface which all DB objects implement:

export interface INode {
    _id: ObjectId
    object_type: string
}

export const Node = interfaceType({
    name: "Node",
    sourceType: {
        module: __filename,
        export: "INode",
    },
    // @ts-ignore
    resolveType(data) {
        return data.object_type
    },
    definition(t) {
        t.nonNull.id("id", {
            resolve(data) {
                return data._id.toString()
            },
        })
        t.nonNull.id("_id", {
            resolve: ({ _id }) => _id.toString(),
        })
        t.nonNull.date("createdAt")
        t.nonNull.date("updatedAt")
        t.date("deletedAt")
        t.nonNull.string("object_type")
        t.string("code")
    },
})

and since i'm using mongo, i want to map its default "_id" to "id" field in schema. The problem here is, do i really have to keep both "id" and "_id" in the interface? I don't want both to appear in my schema and be queryable, i just want the "id", but when i delete the "_id" field from the interface and trying to resolve some field from an object that implements the "node" interface like, i'm obviously getting an error that "_id" does not exist but i need a type for that because the _id is what mongo sends back, this forces me to keep both _id and id in my schema, thus making both queryable. having source type with that _id field does not help either, objects that implement the node don't see it. Is there a way around that to generate just TS for a field but don't put the field to schema?

tgriesser commented 2 years ago

Opened #1106 to help with this

lmanerich commented 2 years ago

hi @tgriesser this still doesn't solve how to link to another generated type

objectType({
    name: 'Post',
    definition(t) {
        t.id('id');
        t.string('label', {
            resolve: PostResolver.label,
            sourceType: 'string',
        });
        t.field('user', {
            type: 'User',
            resolve: PostResolver.user,
            sourceType: 'User',
        });
    },
}

Ends up generating:

export interface NexusGenObjects {
  Query: {};
  Post: { // root type
    id?: string | null; // ID
    label: string
    user: User ///// NOT DEFINED
  }

As a workaround it is possible to declare the type as

sourceType: "NexusGenRootTypes['User']",
VincentGillot commented 1 year ago

Is there any news on this? Because if I use: sourceType: { module: __filename, export: 'UserShape' } The type is correctly imported in the generated types file but since I use this file in the front-end I don't have access to the import.

georgekrax commented 1 year ago

@VincentGillot Unfortunately, there has not been any update on this issue, and I am not aware wether they continue to maintain the Nexus project yet.