[FEATURE] GraphQL mutation support (operationType, variables payload, errors-in-200 selection)
@jeffreyaven is already working on this.
Since Sep 14, 2026.
- Dominant language
- Go
- Stars
- 0
- Forks
- 1
- Avg merge
- 14h 11m
- Merged PRs (30d)
- 8
Description
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 (singleinputobject argument,<entity>Create/<entity>Update/<entity>Destroynaming,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) andbuildkite.
Proposed design
-
Model (
internal/anysdk/graphql.go, loader ininternal/anysdk/loader.goviaExtensionKeyGraphQL):operationType(string, optional): absent orquery-> 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 sametext/templateinputs 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-levelerrorsarray and payload-leveluserErrors/errorsfields, selectable per method).responseSelectionretains its current meaning for the mutation payload projection.
-
Executor (
pkg/graphql/): a one-shotGQLExecutoralongsideStandardGQLReader- no cursor loop, renders query + variables, single request, applieserrorSelectionthenresponseSelection. 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 declarevariables- a robustness improvement independent of mutations.
- Note: variables support should also be made available to the read path in a follow-up, replacing the current escape-and-embed rendering in
-
Invocation wiring: route GraphQL methods with
operationType: mutationthrough the mutation path (theisMutationplumbing inpublic/formulation/formulation.goandpublic/providerinvokers/anysdkhttp/invoker.go) so INSERT/UPDATE/DELETE/EXEC verb mappings insqlVerbsbecome legal for GraphQL methods. -
Static analysis (
public/discovery/static_analyzer.go): retire the TODO at line 1448 with GraphQL method checks -queryparses, template placeholders resolvable from declared parameters,operationType: mutationmethods carryerrorSelection(warning if absent),responseSelectionpresent 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 -operationTypeabsent implies current behaviour. - A mutation method loads via
ExtensionKeyGraphQL, executes one-shot with a variables payload, projectsresponseSelectionon success. - A populated
errorSelectionresult surfaces as a call failure (with the selected content in the error), never as rows - covered by a negative test. - Top-level GraphQL
errorson HTTP 200 also surfaces as failure. - Static analyzer covers GraphQL methods per point 4; the line 1448 TODO is removed.
- Unit tests in
pkg/graphqlfor 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).
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.