Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f6d76f5
feat: provide map of field arguments in operation context
dkorittki Dec 2, 2025
ec45e9e
chore: use a walker
dkorittki Dec 10, 2025
c9cc1e6
fix: only map arguments when needed
dkorittki Dec 11, 2025
9bf3562
chore: clean up
dkorittki Dec 11, 2025
8477e1a
chore: rename FieldArguments() to Arguments()
dkorittki Dec 11, 2025
c5d995f
chore: add unit tests
dkorittki Dec 11, 2025
4800a8c
chore: improve godoc
dkorittki Dec 11, 2025
130bd22
chore: add router tests
dkorittki Dec 11, 2025
9f0c25f
chore: simplify Get method
dkorittki Dec 11, 2025
08ca36e
Merge branch 'main' into dominik/eng-8582-support-access-to-field-arg…
dkorittki Dec 12, 2025
b28d9a7
chore: remove garbage, add comments
dkorittki Dec 12, 2025
a63ff06
fix: log a warning when value cant resolve
dkorittki Dec 12, 2025
d6fcf63
chore: add test verifying direct value resolving
dkorittki Dec 12, 2025
cdca088
chore: nil checks
dkorittki Dec 12, 2025
b9cbf97
fix: support aliased fields
dkorittki Dec 12, 2025
a14b7c4
fix: check report for errors
dkorittki Dec 12, 2025
33d8dfc
chore: fix typo in comments
dkorittki Dec 12, 2025
1c1490e
Merge branch 'main' into dominik/eng-8582-support-access-to-field-arg…
dkorittki Dec 15, 2025
0766e76
chore: use custom type for field args
dkorittki Jan 2, 2026
5dbc44a
feat: use engines field arg mapping
dkorittki Jan 26, 2026
9395881
fix: avoid map creation during request processing
dkorittki Jan 27, 2026
9f0e470
feat: populate option to disable field arg mapping
dkorittki Feb 2, 2026
b102472
chore: use corresponding graph-go-tools version
dkorittki Feb 2, 2026
86ce4f7
Merge branch 'main'
dkorittki Feb 2, 2026
72b7375
fix: prefix with mutation
dkorittki Feb 2, 2026
d8d667b
Merge branch 'main'
dkorittki Feb 9, 2026
c7fd52a
Merge branch 'main'
dkorittki Feb 10, 2026
ac1ecc9
fix: go mod tidy
dkorittki Feb 10, 2026
9bf1f6a
chore: add inline fragment tests + refactor tests
dkorittki Feb 11, 2026
b68b33b
Merge branch 'main'
dkorittki Feb 12, 2026
75244d3
Merge branch 'main' into dominik/eng-8582-support-access-to-field-arg…
dkorittki Feb 23, 2026
4a28b25
chore: add test for asserting unknown paths
dkorittki Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions router/core/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ type OperationContext interface {
Hash() uint64
// Content is the content of the operation
Content() string
FieldArguments() map[string]map[string]any
// Variables is the variables of the operation
Variables() *astjson.Value
// ClientInfo returns information about the client that initiated this operation
Expand Down Expand Up @@ -524,10 +525,11 @@ type operationContext struct {
// RawContent is the raw content of the operation
rawContent string
// Content is the normalized content of the operation
content string
variables *astjson.Value
files []*httpclient.FileUpload
clientInfo *ClientInfo
content string
fieldArguments map[string]map[string]any
variables *astjson.Value
files []*httpclient.FileUpload
clientInfo *ClientInfo
// preparedPlan is the prepared plan of the operation
preparedPlan *planWithMetaData
traceOptions resolve.TraceOptions
Expand Down Expand Up @@ -558,6 +560,10 @@ func (o *operationContext) Variables() *astjson.Value {
return o.variables
}

func (o *operationContext) FieldArguments() map[string]map[string]any {
return o.fieldArguments
}
Comment thread
dkorittki marked this conversation as resolved.

func (o *operationContext) Files() []*httpclient.FileUpload {
return o.files
}
Expand Down
5 changes: 5 additions & 0 deletions router/core/graphql_prehandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,11 @@ func (h *PreHandler) handleOperation(w http.ResponseWriter, req *http.Request, v
requestContext.operation.rawContent = operationKit.parsedOperation.Request.Query
requestContext.operation.content = operationKit.parsedOperation.NormalizedRepresentation
requestContext.operation.variables, err = variablesParser.ParseBytes(operationKit.parsedOperation.Request.Variables)
requestContext.operation.fieldArguments = mapFieldArguments(
operationKit.kit.doc,
requestContext.operation.variables,
operationKit.parsedOperation.RemapVariables,
)
if err != nil {
rtrace.AttachErrToSpan(engineNormalizeSpan, err)
if !requestContext.operation.traceOptions.ExcludeNormalizeStats {
Expand Down
100 changes: 100 additions & 0 deletions router/core/operation_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"golang.org/x/sync/singleflight"

"github.com/wundergraph/astjson"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
Expand Down Expand Up @@ -92,6 +93,105 @@ func (p *OperationPlanner) preparePlan(ctx *operationContext) (*planWithMetaData
return out, nil
}

// mapFieldArguments returns all field arguments inside doc as a map.
//
// The key of the map is a dot-notated path, where each element refers
// to a field. The inner map holds the argument names as keys.
// map["rootfield1.subfield1"]["arg1"]
// map["rootfield1.subfield1"]["arg2"]
// map["rootfield1.subfield2"]["arg1"]
// map["rootfield1.subfield2"]["arg2"]
//
// The value of the inner map is the literal value of the argument.
// Variables will be resolved in case they have been used for arguments.
func mapFieldArguments(doc *ast.Document, vars *astjson.Value, remapVariables map[string]string) map[string]map[string]any {
// TODO: Currently we only map root field arguments. I.e.
// map["rootfield1"]["arg1"]
// map["rootfield2"]["arg1"]
// Needs to extended to support subfields, like
// map["rootfield1.subfield1"]["arg1"]
selectionSet := doc.OperationDefinitions[0].SelectionSet
fields := doc.SelectionSetFieldSelections(selectionSet)

args := make(map[string]map[string]any, len(fields))

for _, field := range fields {
fieldRef := doc.Selections[field].Ref
fieldName := doc.FieldNameString(fieldRef)
fieldArgs := doc.FieldArguments(fieldRef)

for _, fieldArg := range fieldArgs {
m := make(map[string]any, len(fieldArgs))
args[fieldName] = m

argName := doc.ArgumentNameString(fieldArg)
val := doc.Arguments[fieldArg].Value
args[fieldName][argName] = getArgValue(doc, val, vars, remapVariables)
Comment thread
dkorittki marked this conversation as resolved.
Outdated
}
}

return args
}
Comment thread
dkorittki marked this conversation as resolved.
Outdated

// getArgValue returns the actual value of val.
// It resolves variables in case these have been used for arguments,
// else it will use values from doc.
func getArgValue(doc *ast.Document, val ast.Value, variables *astjson.Value, remapVariables map[string]string) any {
if val.Kind != ast.ValueKindVariable {
// TODO delete this comment.
// I observed we never actually hit this code path because the operation parser
// automically creates variables and maps them to arguments, even if no initial variables are provided.
// We should probably still have this in place just in case.
// Maybe there is a better way than to to use ValueToJSON but I haven't found one yet.
actualValue, err := doc.ValueToJSON(val)
if err != nil {
// TODO error handling
return nil
}
// TODO: Type cast to what this actually is, it returns only []byte atm
return actualValue
}

varName := doc.VariableValueNameString(val.Ref)
originalVarName := varName
if remapVariables != nil {
if original, ok := remapVariables[varName]; ok {
originalVarName = original
}
}

varValue := variables.Get(originalVarName)
switch varValue.Type() {
case astjson.TypeNumber:
return varValue.GetInt()
case astjson.TypeString:
return string(varValue.GetStringBytes())
case astjson.TypeObject:
// TODO maybe create map out of varValue to give hook developers a better experience.
// The problem is this would be a nested operation because objects can contain all kinds
// of children elements, such as numbers, strings, other objects, etc.
// Right now we only return the astjson type directly and leave it to the hook developer
// to work with that.
return varValue.GetObject()
case astjson.TypeArray:
// TODO maybe create slice out of varValue to give hook developers a better experience.
// The problem is this would be a nested operation because arrays can contain all kinds
// of children elements, such as numbers, strings, other objects, etc.
// Right now we only return the astjson type directly and leave it to the hook developer
// to work with that.
return varValue.GetArray()
case astjson.TypeFalse:
// TODO hypothetically written, needs testing
return false
case astjson.TypeTrue:
// TODO hypothetically written, needs testing
return true
default:
// TODO test for type = null
return nil
}
}

type PlanOptions struct {
ClientInfo *ClientInfo
TraceOptions resolve.TraceOptions
Expand Down
1 change: 1 addition & 0 deletions router/core/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi
if err != nil {
return nil, nil, err
}
opContext.fieldArguments = mapFieldArguments(operationKit.kit.doc, opContext.variables, opContext.remapVariables)

startValidation := time.Now()

Expand Down
Loading