Skip to content

[FEATURE] GraphQL mutation support (operationType, variables payload, errors-in-200 selection) #142

Description

@jeffreyaven

Summary

any-sdk currently supports GraphQL for reads only. The x-stackQL-graphQL extension is modelled by standardGraphQL (internal/anysdk/graphql.go) with id, query, url, httpVerb, cursor, and responseSelection - no operation type and no variables. The only execution path is StandardGQLReader (pkg/graphql/graphql.go), a paginating read loop (cursor strategies: cursor_after, keyset, offset, page_info) whose renderQuery() templates the cursor splice into the query string and posts { "query": "..." }. The REST mutation machinery (isMutation in public/formulation and public/providerinvokers/anysdkhttp) never consults GetGraphQL(), and the static analyzer short-circuits GraphQL methods entirely:

// public/discovery/static_analyzer.go:1448
graphQL := method.GetGraphQL()
if graphQL != nil {
    return result // TODO: GraphQL methods analysis
}

The consequence is that all GraphQL-backed providers are SELECT-only by engine constraint, not by provider choice. This issue proposes first-class GraphQL mutation support.

Motivating consumers

  • gitlab - the intended canonical reference implementation: a strictly Relay-conventional mutation surface (single input object argument, <entity>Create / <entity>Update / <entity>Destroy naming, errors: [] payloads inside HTTP 200), free full-lifecycle CI via the gitlab-ce container. A read-only provider for a DevOps platform is a visible gap (issues, MRs, labels, CI variables are write-expected resources).
  • runpod - GraphQL-only vendor whose control operations (pod deploy/start/stop/terminate) are mutations; without this feature the provider is inventory-only.
  • railway - GraphQL-only vendor; deploys/restarts/scaling are mutations.
  • Write phases of newrelic (NerdGraph: alert policies, dashboards, tagging) and buildkite.

Proposed design

  1. Model (internal/anysdk/graphql.go, loader in internal/anysdk/loader.go via ExtensionKeyGraphQL):

    • operationType (string, optional): absent or query -> existing reader behaviour; mutation -> one-shot executor. Back-compat: every existing spec is untouched.
    • variables (map, optional): a template map binding method parameters to a GraphQL variables payload, rendered with the same text/template inputs the query template receives. Present -> the request body is { "query": ..., "variables": { ... } }.
    • errorSelection (GraphQLElement, optional): a jsonPath whose resolution to a non-empty array or non-null value marks the call failed, regardless of HTTP 200. This is the GraphQL errors-in-200 convention (both the top-level errors array and payload-level userErrors/errors fields, selectable per method).
    • responseSelection retains its current meaning for the mutation payload projection.
  2. Executor (pkg/graphql/): a one-shot GQLExecutor alongside StandardGQLReader - no cursor loop, renders query + variables, single request, applies errorSelection then responseSelection. Reuse the client, logging context (ContextWithHTTPLogger), and stream_transform hooks the reader already has.

    • Note: variables support should also be made available to the read path in a follow-up, replacing the current escape-and-embed rendering in renderQuery() (pkg/graphql/graphql.go:649) for methods that declare variables - a robustness improvement independent of mutations.
  3. Invocation wiring: route GraphQL methods with operationType: mutation through the mutation path (the isMutation plumbing in public/formulation/formulation.go and public/providerinvokers/anysdkhttp/invoker.go) so INSERT/UPDATE/DELETE/EXEC verb mappings in sqlVerbs become legal for GraphQL methods.

  4. Static analysis (public/discovery/static_analyzer.go): retire the TODO at line 1448 with GraphQL method checks - query parses, template placeholders resolvable from declared parameters, operationType: mutation methods carry errorSelection (warning if absent), responseSelection present for selectable methods, cursor config forbidden on mutations.

Spec shape (target)

x-stackQL-graphQL:
  url: https://gitlab.com/api/graphql
  httpVerb: POST
  operationType: mutation
  query: >
    mutation($input: CreateIssueInput!) {
      createIssue(input: $input) {
        issue { iid title state webUrl }
        errors
      }
    }
  variables:
    input:
      projectPath: '{{ .full_path }}'
      title: '{{ .title }}'
      description: '{{ .description }}'
  responseSelection:
    jsonPath: '$.data.createIssue.issue'
  errorSelection:
    jsonPath: '$.data.createIssue.errors'

Acceptance criteria

  • Existing GraphQL read specs (including the in-repo fixture internal/anysdk/testdata/registry/src/github/v0.3.1/services/scim.yaml) load and execute byte-identically - operationType absent implies current behaviour.
  • A mutation method loads via ExtensionKeyGraphQL, executes one-shot with a variables payload, projects responseSelection on success.
  • A populated errorSelection result surfaces as a call failure (with the selected content in the error), never as rows - covered by a negative test.
  • Top-level GraphQL errors on HTTP 200 also surfaces as failure.
  • Static analyzer covers GraphQL methods per point 4; the line 1448 TODO is removed.
  • Unit tests in pkg/graphql for the executor (success, payload errors, top-level errors, template resolution failure).

Out of scope

  • GraphQL subscriptions (streaming - excluded per standing policy).
  • Automatic mutation generation from introspection (provider tooling concern - @stackql/provider-utils).
  • Cursor/pagination interaction with mutations (forbidden by analyzer rule instead).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions