Skip to content
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

"An operation name is required if the query contains multiple operations" #406

Closed
Nasr-Ladib opened this issue Sep 22, 2022 · 7 comments · Fixed by #629
Closed

"An operation name is required if the query contains multiple operations" #406

Nasr-Ladib opened this issue Sep 22, 2022 · 7 comments · Fixed by #629
Labels
released Status: Up for grabs Issues that are ready to be worked on by anyone Type: Support Any questions, information, or general needs around the SDK or GitHub APIs

Comments

@Nasr-Ladib
Copy link

Nasr-Ladib commented Sep 22, 2022

Hi. I am trying to put many queries and fragments inside one request.
My POST request's body:

query login {
  createSession(email:"test@123.com", password: "foobar"){
    session
  }
}

query me {
  deserializeUser{
    id,
    email,
    name
  }
}

I found that I can add ./src/graphql.ts:11

const NON_VARIABLE_OPTIONS = ["method", "baseUrl", "url", "headers", "request", "query", "mediaType"]

add a non-variable option operationName

const NON_VARIABLE_OPTIONS = ["method", "baseUrl", "url", "headers", "request", "query", "mediaType","operationName"]

Please give me some clues. Thanks in advance.

@gr2m gr2m added the Type: Support Any questions, information, or general needs around the SDK or GitHub APIs label Sep 23, 2022
@gr2m
Copy link
Contributor

gr2m commented Sep 23, 2022

It looks like an error you are getting from the GraphQL API, I don't think this is an error with @octokit/graphql

@Nasr-Ladib
Copy link
Author

Nasr-Ladib commented Oct 6, 2022

Actually, I can't pass the parameter on the body of operationName.
Because the request needs (query and operationName) of a multi operation on the same graphQL.

I have queryFile has multiple operations as the following example:

query login {
  createSession(email:"test@123.com", password: "foobar"){
    session
  }
}

query me {
  deserializeUser{
    id,
    email,
    name
  }
}

So, I want to use the login operation for the first time than the me operation.

const result = await this.auth.graphql( queryFile, operationName );

@klippx
Copy link
Contributor

klippx commented Jan 30, 2025

Guys, this is regular/plain stuff: https://graphql.org/learn/serving-over-http/#post-request-and-body

It is quite amazing to me that this issue has been open over 3 years and received no attention. It is quite inconvenient to be forced to put everything in separate documents because your library doesn't support operationName,

@Nasr-Ladib has a perfectly valid use case here, a query is being sent that contains more than one operation. Then, according to the spec, he has to point out which operation he wants to execute in this query. This is done via operationName which should be treated as a special variable option.

Here is a scenario:

Imagine having two different queries for GitHub pull requests. Both are using the same fragment but each have slightly different input filters to the root query.

Our document RepositoryPullRequestsFilteredByLabel.gql:

fragment PullRequestFields on PullRequest {
  # 100+ lines of fields here
}

query RepositoryPullRequestsFilteredByLabel(
  $owner: String!
  $name: String!
  $label: String!
  $batchSize: Int = 10
  $cursor: String
  $labelsBatchSize: Int = 5
  $labelsCursor: String
) {
  repository(owner: $owner, name: $name) {
    pullRequests(
      states: OPEN
      first: $batchSize
      after: $cursor
      labels: [$label]
      orderBy: { field: CREATED_AT, direction: DESC }
    ) {
      nodes {
        ...PullRequestFields
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

query RepositoryPullRequests(
  $owner: String!
  $name: String!
  $batchSize: Int = 10
  $cursor: String
  $labelsBatchSize: Int = 5
  $labelsCursor: String
) {
  repository(owner: $owner, name: $name) {
    pullRequests(
      states: OPEN
      first: $batchSize
      after: $cursor
      orderBy: { field: CREATED_AT, direction: DESC }
    ) {
      nodes {
        ...PullRequestFields
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

The query RepositoryPullRequestsFilteredByLabel only works if you pass labels, so we need to write a separate query RepositoryPullRequests to get rid of $labels if we don't want to filter based on label. However, we want them in the same file so we don't duplicate the fragment.

Here is how we want to use it:

import RepositoryPullRequestsFilteredByLabel from './RepositoryPullRequestsFilteredByLabel.gql'

      await graphql({
        query: print(RepositoryPullRequestsFilteredByLabel),    // Contains two operations + 1 fragment
        operationName: 'RepositoryPullRequestsFilteredByLabel', // Point out which operation we want to use; RepositoryPullRequestsFilteredByLabel or RepositoryPullRequests
        owner: ownerName,
        name: repoName,
        label,
        batchSize,
        cursor,
        labelsBatchSize,
        labelsCursor,
      })

However the library does not support this.

@klippx
Copy link
Contributor

klippx commented Jan 30, 2025

Here is a workaround:

extract-operation.ts:

import {
  type DocumentNode,
  type FragmentDefinitionNode,
  Kind,
  type OperationDefinitionNode,
} from 'graphql'

export function extractOperation(
  doc: DocumentNode,
  operationName: string,
): DocumentNode {
  const operation = doc.definitions.find(
    (def): def is OperationDefinitionNode =>
      def.kind === Kind.OPERATION_DEFINITION &&
      def.name?.value === operationName,
  )

  if (!operation) {
    throw new Error(
      `Operation "${operationName}" not found in the provided DocumentNode`,
    )
  }

  // Collect all fragment definitions
  const fragmentDefinitions = new Map<string, FragmentDefinitionNode>()
  doc.definitions.forEach((def) => {
    if (def.kind === Kind.FRAGMENT_DEFINITION) {
      fragmentDefinitions.set(def.name.value, def)
    }
  })

  // Find fragments that are actually used in the operation
  const usedFragments = new Set<string>()

  function collectUsedFragments(node: any) {
    if (node.kind === Kind.FRAGMENT_SPREAD) {
      if (!usedFragments.has(node.name.value)) {
        usedFragments.add(node.name.value)
        const fragment = fragmentDefinitions.get(node.name.value)
        if (fragment) {
          collectUsedFragments(fragment)
        }
      }
    } else if (node.selectionSet) {
      node.selectionSet.selections.forEach(collectUsedFragments)
    }
  }

  collectUsedFragments(operation)

  // Include only the operation and necessary fragments
  return {
    ...doc,
    definitions: [
      operation,
      ...Array.from(usedFragments).map(
        (name) => fragmentDefinitions.get(name)!,
      ),
    ],
  }
}

Updated example usage code:

import Raw from './RepositoryPullRequestsFilteredByLabel.gql'

const RepositoryPullRequestsFilteredByLabel = extractOperation(
  Raw,
  'RepositoryPullRequestsFilteredByLabel',
)

const RepositoryPullRequests = extractOperation(
  Raw,
  'RepositoryPullRequests',
)

// ...

await graphql({
  query: print(RepositoryPullRequestsFilteredByLabel), // Use the appropriate query
  owner: ownerName,
  name: repoName,
  label,
  batchSize,
  cursor,
  labelsBatchSize,
  labelsCursor,
})

Now we can keep everything in one file and still use @octokit/graphql with only this mild inconvenience 😬

@wolfy1339 wolfy1339 moved this from 🆕 Triage to 🔥 Backlog in 🧰 Octokit Active Jan 30, 2025
@wolfy1339 wolfy1339 added the Status: Up for grabs Issues that are ready to be worked on by anyone label Jan 30, 2025
@wolfy1339
Copy link
Member

Over the last couple years there have been changes to the people working on Octokit.

Previously there was gr2m that was dedicated full time working on JS Octokit.
Then GitHub instead brought on a whole new team for all the Octokit libraries across all the languages.

There are a few community contributors who have push access.

I personally can't help, I don't know anything about GraphQL.

Besides, it's not just this repo that is feeling the effects where there are issues that are a couple years old.

The whole of JS Octokit is lacking attention

@klippx
Copy link
Contributor

klippx commented Jan 31, 2025

@wolfy1339 Sure, I opened a PR for it! #629

And also a types PR: octokit/types.ts#662

@github-project-automation github-project-automation bot moved this from 🔥 Backlog to ✅ Done in 🧰 Octokit Active Jan 31, 2025
Copy link

🎉 This issue has been resolved in version 8.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
released Status: Up for grabs Issues that are ready to be worked on by anyone Type: Support Any questions, information, or general needs around the SDK or GitHub APIs
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants