Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions v2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Features

* add `BypassIfValuesNull` flag to subscription field filters so null or absent templated `IN` values can match
* support costs on arguments of directives ([#1465](https://github.com/wundergraph/graphql-go-tools/issues/1465)) ([2eca1ab](https://github.com/wundergraph/graphql-go-tools/commit/2eca1ab9932395d9b3181d28e8e6c2c03818f68b))


Expand Down
5 changes: 3 additions & 2 deletions v2/pkg/engine/plan/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ type SubscriptionFilterCondition struct {
}

type SubscriptionFieldCondition struct {
FieldPath []string
Values []string
FieldPath []string
Values []string
BypassIfValuesNull bool
}

type ArgumentsConfigurations []ArgumentConfiguration
Expand Down
1 change: 1 addition & 0 deletions v2/pkg/engine/plan/path_builder_visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@ func (c *pathBuilderVisitor) buildSubscriptionFilterCondition(condition Subscrip
func (c *pathBuilderVisitor) buildSubscriptionFieldFilter(condition *SubscriptionFieldCondition) *resolve.SubscriptionFieldFilter {
filter := &resolve.SubscriptionFieldFilter{}
filter.FieldPath = condition.FieldPath
filter.BypassIfValuesNull = condition.BypassIfValuesNull
filter.Values = make([]resolve.InputTemplate, len(condition.Values))
for i, value := range condition.Values {
matches := argument_templates.ArgumentTemplateRegex.FindAllStringSubmatchIndex(value, -1)
Expand Down
16 changes: 16 additions & 0 deletions v2/pkg/engine/plan/subscription_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,19 @@ func TestPlanSubscriptionFilter(t *testing.T) {
},
))
}

func TestBuildSubscriptionFilterConditionPropagatesBypassIfValuesNull(t *testing.T) {
visitor := &pathBuilderVisitor{}

filter := visitor.buildSubscriptionFilterCondition(SubscriptionFilterCondition{
In: &SubscriptionFieldCondition{
FieldPath: []string{"id"},
Values: []string{`"1"`},
BypassIfValuesNull: true,
},
})

require.NotNil(t, filter)
require.NotNil(t, filter.In)
assert.True(t, filter.In.BypassIfValuesNull)
}
21 changes: 19 additions & 2 deletions v2/pkg/engine/resolve/subscription_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ type SubscriptionFilter struct {
}

type SubscriptionFieldFilter struct {
FieldPath []string
Values []InputTemplate
FieldPath []string
Values []InputTemplate
BypassIfValuesNull bool
}

func (f *SubscriptionFilter) SkipEvent(ctx *Context, data []byte) (bool, error) {
Expand Down Expand Up @@ -90,6 +91,22 @@ func (f *SubscriptionFieldFilter) SkipEvent(ctx *Context, data []byte) (bool, er
return false, nil
}

if f.BypassIfValuesNull && ctx != nil {
for i := range f.Values {
if len(f.Values[i].Segments) != 1 {
continue
}
seg := f.Values[i].Segments[0]
if seg.SegmentType != VariableSegmentType {
continue
}
value := ctx.Variables.Get(seg.VariableSourcePath...)
if value == nil || value.Type() == astjson.TypeNull {
return false, nil
}
}
}

expected, expectedDataType, _, _err := jsonparser.Get(data, f.FieldPath...)
if _err != nil {
return true, nil
Expand Down
187 changes: 187 additions & 0 deletions v2/pkg/engine/resolve/subscription_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,190 @@ func TestSubscriptionFilter(t *testing.T) {
assert.Equal(t, false, skip)
})
}

func TestSubscriptionFieldFilterBypassIfValuesNull(t *testing.T) {
variableValue := func(path ...string) InputTemplate {
return InputTemplate{
Segments: []TemplateSegment{
{
SegmentType: VariableSegmentType,
VariableKind: ContextVariableKind,
VariableSourcePath: path,
Renderer: NewPlainVariableRenderer(),
},
},
}
}

multiSegmentValue := func(path ...string) InputTemplate {
return InputTemplate{
Segments: []TemplateSegment{
{
SegmentType: VariableSegmentType,
VariableKind: ContextVariableKind,
VariableSourcePath: path,
Renderer: NewPlainVariableRenderer(),
},
{
SegmentType: StaticSegmentType,
Data: []byte(`-suffix`),
},
},
}
}

staticValue := func(value string) InputTemplate {
return InputTemplate{
Segments: []TemplateSegment{
{
SegmentType: StaticSegmentType,
Data: []byte(value),
},
},
}
}

tests := []struct {
name string
bypassIfValuesNull bool
variables string
data string
values []InputTemplate
expectedSkip bool
}{
{
name: "flag false variable absent skips",
variables: `{}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: true,
},
{
name: "flag false variable explicit null matches event null",
variables: `{"args":{"x":null}}`,
data: `{"productName":null}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag false variable explicit null skips non-null event",
variables: `{"args":{"x":null}}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: true,
},
{
name: "flag true variable absent bypasses when event field exists",
bypassIfValuesNull: true,
variables: `{}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true variable absent bypasses before event lookup",
bypassIfValuesNull: true,
variables: `{}`,
data: `{"other":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true variable explicit null bypasses",
bypassIfValuesNull: true,
variables: `{"args":{"x":null}}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true variable value matches",
bypassIfValuesNull: true,
variables: `{"args":{"x":"foo"}}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true variable value mismatches",
bypassIfValuesNull: true,
variables: `{"args":{"x":"foo"}}`,
data: `{"productName":"bar"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: true,
},
{
name: "flag true multi-segment value with nil variable does not bypass",
bypassIfValuesNull: true,
variables: `{}`,
data: `{"productName":"bar"}`,
values: []InputTemplate{multiSegmentValue("args", "a")},
expectedSkip: true,
},
{
name: "flag true empty array variable does not bypass",
bypassIfValuesNull: true,
variables: `{"args":{"x":[]}}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: true,
},
{
name: "flag true array variable matches element",
bypassIfValuesNull: true,
variables: `{"args":{"x":["foo","bar"]}}`,
data: `{"productName":"bar"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true array containing null does not bypass",
bypassIfValuesNull: true,
variables: `{"args":{"x":[null]}}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{variableValue("args", "x")},
expectedSkip: true,
},
{
name: "flag true mixed values bypass on null variable",
bypassIfValuesNull: true,
variables: `{"args":{"x":null}}`,
data: `{"productName":"bar"}`,
values: []InputTemplate{staticValue(`"static"`), variableValue("args", "x")},
expectedSkip: false,
},
{
name: "flag true static-only value matches as before",
bypassIfValuesNull: true,
variables: `{}`,
data: `{"productName":"foo"}`,
values: []InputTemplate{staticValue(`"foo"`)},
expectedSkip: false,
},
{
name: "flag true static-only value mismatches as before",
bypassIfValuesNull: true,
variables: `{}`,
data: `{"productName":"bar"}`,
values: []InputTemplate{staticValue(`"foo"`)},
expectedSkip: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := &SubscriptionFieldFilter{
FieldPath: []string{"productName"},
Values: tt.values,
BypassIfValuesNull: tt.bypassIfValuesNull,
}
c := &Context{
Variables: astjson.MustParseBytes([]byte(tt.variables)),
}
skip, err := filter.SkipEvent(c, []byte(tt.data))

assert.NoError(t, err)
assert.Equal(t, tt.expectedSkip, skip)
})
}
}
Loading