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
-
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.
-
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.
-
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.
-
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).
Summary
any-sdk currently supports GraphQL for reads only. The
x-stackQL-graphQLextension is modelled bystandardGraphQL(internal/anysdk/graphql.go) withid,query,url,httpVerb,cursor, andresponseSelection- no operation type and no variables. The only execution path isStandardGQLReader(pkg/graphql/graphql.go), a paginating read loop (cursor strategies:cursor_after,keyset,offset,page_info) whoserenderQuery()templates the cursor splice into the query string and posts{ "query": "..." }. The REST mutation machinery (isMutationinpublic/formulationandpublic/providerinvokers/anysdkhttp) never consultsGetGraphQL(), and the static analyzer short-circuits GraphQL methods entirely: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.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.renderQuery()(pkg/graphql/graphql.go:649) for methods that declarevariables- a robustness improvement independent of mutations.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)
Acceptance criteria
internal/anysdk/testdata/registry/src/github/v0.3.1/services/scim.yaml) load and execute byte-identically -operationTypeabsent implies current behaviour.ExtensionKeyGraphQL, executes one-shot with a variables payload, projectsresponseSelectionon success.errorSelectionresult surfaces as a call failure (with the selected content in the error), never as rows - covered by a negative test.errorson HTTP 200 also surfaces as failure.pkg/graphqlfor the executor (success, payload errors, top-level errors, template resolution failure).Out of scope
@stackql/provider-utils).