shurcooL / githubv4

Package githubv4 is a client library for accessing GitHub GraphQL API v4 (https://docs.github.com/en/graphql).
MIT License
1.1k stars 89 forks source link

Type mismatch between variable and argument for optional String / ID #99

Closed andyfeller closed 2 years ago

andyfeller commented 2 years ago

In trying to query the GitHub repositoryOwner for retrieving repositories for organizations and/or users, I ran into a problem involving the workaround from https://github.com/shurcooL/githubv4/issues/12 as the interface does not appear to like ID for EndCursor.

When calling this query, the following error is returned from GitHub GraphQL API:

Error: Message: Type mismatch on variable $endCursor and argument after (ID / String), Locations: [{Line:1 Column:101}]

type reposQuery struct {
    RepositoryOwner struct {
        Repositories struct {
            Nodes []struct {
                Name string
            }
            PageInfo struct {
                HasNextPage bool
                EndCursor   string
            }
        } `graphql:"repositories(first: 100, after: $endCursor, ownerAffiliations: [OWNER])"`
    } `graphql:"repositoryOwner(login: $owner)"`
}

func getRepos(owner string, endCursor *string) (*reposQuery, error) {
    query := new(reposQuery)
    variables := map[string]interface{}{
        "owner":     graphql.String(owner),
        "endCursor": endCursor,
    }

    err := client.Query("getRepos", query, variables)

    return query, err
}

In stepping through the code, the error emerges in decoding the response from the server where the query is generated as:

"query getRepos($endCursor:ID$owner:String!){repositoryOwner(login: $owner){repositories(first: 100, after: $endCursor, ownerAffiliations: [OWNER]){nodes{name},pageInfo{hasNextPage,endCursor}}}}"

Any assistance would be greatly appreciated 🙇

andyfeller commented 2 years ago

For reference, this query works with the interface mentioned:

query($owner: String!, $endCursor: String) {
    repositoryOwner(login: $owner) {
        repositories(first: 100, ownerAffiliations: [OWNER], after: $endCursor) {
            pageInfo {
                hasNextPage
                endCursor
            }
            nodes {
                name
                nameWithOwner
            }
        }
    }
}
dmitshur commented 2 years ago

If you look at GitHub's RepositoryOwner interface, the after argument of the repositories field has type String, so that's what needs to be provided. Your code can look something like:

variables := map[string]interface{}{
    "owner":     githubv4.String(owner),
    "endCursor": githubv4.String(endCursor),
}

See here for an example of similar code.

andyfeller commented 2 years ago

Thanks for the follow up, @dmitshur ! That is what I initially did, however because the endpoint wants endCursor to be optional, I had to change it to a pointer. Does that make sense?

dmitshur commented 2 years ago

Yes, if it's an optional GraphQL type, using a Go pointer makes sense. That is also what's done in the code example I linked above. The NewString helper exists for convenience of doing this.

andyfeller commented 2 years ago

So, this is where we're coming full circle as when I'm providing the pointer, GitHub is responding back with the following error:

Error: Message: Type mismatch on variable $endCursor and argument after (ID / String), Locations: [{Line:1 Column:101}]

with the underlying generated query being:

"query getRepos($endCursor:ID$owner:String!){repositoryOwner(login: $owner){repositories(first: 100, after: $endCursor, ownerAffiliations: [OWNER]){nodes{name},pageInfo{hasNextPage,endCursor}}}}"

The only thing I can guess is that the repositoryOwner interface does not like $endCursor:ID with however the variable is being sent over.

dmitshur commented 2 years ago

From what you've shared, I'm fairly confident the problem is with the variables map you're providing to the Query call, specifically this code:

query := new(reposQuery)
variables := map[string]interface{}{
    "owner":     graphql.String(owner),
    "endCursor": endCursor,
}

err := client.Query("getRepos", query, variables)

I think changing it to the following should work:

query := new(reposQuery)
variables := map[string]interface{}{
    "owner":     graphql.String(owner),
    "endCursor": graphql.NewString(graphql.String(endCursor)),
}

err := client.Query("getRepos", query, variables)

If that doesn't help, can you post a complete program that reproduces the issue for you?

Finally, take a look at https://github.com/shurcooL/githubv4#pagination if you haven't already, since that is a functional example for pagination.

andyfeller commented 2 years ago

I'll give it a go! (pun not intended) Seriously: thank you for your amazing patience and help ❤️

andyfeller commented 2 years ago
"endCursor": graphql.NewString(graphql.String(endCursor)),

Unfortunately, this doesn't really solve it especially as it gives compiler errors:

cannot convert endCursor (variable of type *string) to graphql.String compiler InvalidConversion

I've made my repo public and the code is here: https://github.com/andyfeller/gh-dependency-report/blob/initial/cmd/root.go#L294

I'm simply trying to run this as:

go run main.go andyfeller 
dmitshur commented 2 years ago

Ah, that snippet assumed that endCursor was of type string, but in your code its type is *string. In that case, you can just convert it to a *graphql.String directly:

func getRepos(owner string, endCursor *string) (*reposQuery, error) {
    query := new(reposQuery)
    variables := map[string]interface{}{
        "owner":     graphql.String(owner),
        "endCursor": (*graphql.String)(endCursor),
    }

    err := client.Query("getRepos", query, variables)

    return query, err
}

With that change, the program compiles and getRepos makes a successful query with repository names populated. (There are other parts of that program that need modification to run without errors, for example repos = make([]string, 100) creates a slice with 100 empty repository names; it should probably be repos = make([]string, 0, 100) to create an empty slice with 100 capacity.)

andyfeller commented 2 years ago

Thank you again for your patience, @dmitshur 🙇

Would you welcome a PR including a section providing an example of this to avoid confusion from other users?

andyfeller commented 2 years ago

With that change, the program compiles and getRepos makes a successful query with repository names populated. (There are other parts of that program that need modification to run without errors, for example repos = make([]string, 100) creates a slice with 100 empty repository names; it should probably be repos = make([]string, 0, 100) to create an empty slice with 100 capacity.)

😅 thanks for the additional advice; it's been a while since my undergraduate days of only doing C development and this is the 3rd time I've tried teaching myself GoLang.

dmitshur commented 2 years ago

Would you welcome a PR including a section providing an example of this to avoid confusion from other users?

I believe this should be covered by https://github.com/shurcooL/githubv4#pagination; can you please let me know if you think something is missing from there?

I'll close this since I understand the question is resolved.