Skip to content

Commit b20a525

Browse files
authored
Merge pull request #81 from PostHog/kefapps/kef-175-normalize-posthog_insight-query_json-state-to-avoid-import
fix: normalize imported insight queries
2 parents 7d009bd + 3fe81d6 commit b20a525

6 files changed

Lines changed: 100 additions & 36 deletions

File tree

internal/resource/hog_function.go

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import (
1818
"github.com/posthog/terraform-provider/internal/util"
1919
)
2020

21+
// hogFunctionServerFields are fields the server generates on hog-function
22+
// responses and should be stripped to avoid spurious diffs.
23+
var hogFunctionServerFields = map[string]struct{}{
24+
"bytecode": {},
25+
"order": {},
26+
}
27+
2128
func NewHogFunction() resource.Resource {
2229
return core.NewGenericResource[HogFunctionResourceTFModel, httpclient.HogFunctionRequest, httpclient.HogFunction](
2330
HogFunctionOps{},
@@ -498,35 +505,6 @@ func normalizeJSONStripServerFields(apiData interface{}, userJSON string) (strin
498505
return "", nil
499506
}
500507

501-
cleaned := stripServerFields(apiData)
508+
cleaned := util.StripFields(apiData, hogFunctionServerFields)
502509
return normalizeJSONForState(cleaned, userJSON)
503510
}
504-
505-
// stripServerFields recursively removes server-computed fields from a value.
506-
func stripServerFields(v interface{}) interface{} {
507-
// serverComputedFields are fields that the server generates and should be stripped
508-
// from API responses to avoid spurious diffs.
509-
var serverComputedFields = map[string]struct{}{
510-
"bytecode": {},
511-
"order": {},
512-
}
513-
514-
switch val := v.(type) {
515-
case map[string]interface{}:
516-
cleaned := make(map[string]interface{})
517-
for k, v := range val {
518-
if _, isServerField := serverComputedFields[k]; !isServerField {
519-
cleaned[k] = stripServerFields(v)
520-
}
521-
}
522-
return cleaned
523-
case []interface{}:
524-
cleaned := make([]interface{}, len(val))
525-
for i, item := range val {
526-
cleaned[i] = stripServerFields(item)
527-
}
528-
return cleaned
529-
default:
530-
return v
531-
}
532-
}

internal/resource/hog_function_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
88
"github.com/hashicorp/terraform-plugin-framework/types"
99
"github.com/posthog/terraform-provider/internal/httpclient"
10+
"github.com/posthog/terraform-provider/internal/util"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
)
@@ -699,7 +700,7 @@ func TestStripServerFields(t *testing.T) {
699700

700701
for name, tt := range tests {
701702
t.Run(name, func(t *testing.T) {
702-
result := stripServerFields(tt.input)
703+
result := util.StripFields(tt.input, hogFunctionServerFields)
703704
assert.Equal(t, tt.expected, result)
704705
})
705706
}

internal/resource/insight.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ import (
1919
"github.com/posthog/terraform-provider/internal/util"
2020
)
2121

22+
// insightQueryServerFields are query-internal fields that PostHog injects on
23+
// response and would otherwise produce drift after import.
24+
var insightQueryServerFields = map[string]struct{}{
25+
"version": {},
26+
"result": {},
27+
"hogql": {},
28+
"is_cached": {},
29+
}
30+
2231
func NewInsight() resource.Resource {
2332
return core.NewGenericResource[InsightResourceTFModel, httpclient.InsightRequest, httpclient.Insight](
2433
InsightOps{},
@@ -251,14 +260,21 @@ func normalizeQueryForState(apiQuery map[string]interface{}, userQueryJSON strin
251260
return "", nil
252261
}
253262

263+
cleanedQuery := util.StripFields(apiQuery, insightQueryServerFields)
264+
265+
userQueryJSON = strings.TrimSpace(userQueryJSON)
266+
if userQueryJSON == "" {
267+
return marshalJSON(cleanedQuery)
268+
}
269+
254270
// Parse user's query to know which fields to keep
255271
var userQuery map[string]interface{}
256272
if err := json.Unmarshal([]byte(userQueryJSON), &userQuery); err != nil {
257-
// If we can't parse user's query, fall back to returning API query as canonical JSON
258-
return marshalJSON(apiQuery)
273+
// If we can't parse user's query, fall back to returning a cleaned API query.
274+
return marshalJSON(cleanedQuery)
259275
}
260276

261-
filtered := filterToOnlyIncludeUserFields(userQuery, apiQuery)
277+
filtered := filterToOnlyIncludeUserFields(userQuery, cleanedQuery)
262278
return marshalJSON(filtered)
263279
}
264280

internal/resource/insight_test.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package resource
22

33
import (
4+
"context"
45
"testing"
56

7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
"github.com/posthog/terraform-provider/internal/httpclient"
69
"github.com/stretchr/testify/assert"
710
"github.com/stretchr/testify/require"
811
)
@@ -192,11 +195,26 @@ func TestNormalizeQueryForState(t *testing.T) {
192195
},
193196
"falls back on invalid user JSON": {
194197
apiQuery: map[string]interface{}{
195-
"kind": "test",
198+
"kind": "test",
199+
"version": float64(2),
196200
},
197201
userQueryJSON: `invalid json`,
198202
expected: `{"kind":"test"}`,
199203
},
204+
"strips server fields when user query is unavailable": {
205+
apiQuery: map[string]interface{}{
206+
"kind": "DataVisualizationNode",
207+
"result": []interface{}{1, 2, 3},
208+
"hogql": "SELECT 1",
209+
"is_cached": true,
210+
"source": map[string]interface{}{
211+
"kind": "TrendsQuery",
212+
"version": float64(2),
213+
},
214+
},
215+
userQueryJSON: "",
216+
expected: `{"kind":"DataVisualizationNode","source":{"kind":"TrendsQuery"}}`,
217+
},
200218
}
201219

202220
for name, tt := range tests {
@@ -207,3 +225,28 @@ func TestNormalizeQueryForState(t *testing.T) {
207225
})
208226
}
209227
}
228+
229+
func TestInsightMapResponseToModel_StripsServerQueryFieldsWithoutUserConfig(t *testing.T) {
230+
ops := InsightOps{}
231+
model := InsightResourceTFModel{
232+
QueryJSON: types.StringNull(),
233+
}
234+
235+
resp := httpclient.Insight{
236+
ID: 123,
237+
Query: map[string]interface{}{
238+
"kind": "DataVisualizationNode",
239+
"result": []interface{}{1, 2, 3},
240+
"hogql": "SELECT 1",
241+
"is_cached": true,
242+
"source": map[string]interface{}{
243+
"kind": "TrendsQuery",
244+
"version": float64(2),
245+
},
246+
},
247+
}
248+
249+
diags := ops.MapResponseToModel(context.Background(), resp, &model)
250+
require.False(t, diags.HasError(), diags.Errors())
251+
assert.Equal(t, `{"kind":"DataVisualizationNode","source":{"kind":"TrendsQuery"}}`, model.QueryJSON.ValueString())
252+
}

internal/util/json.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package util
2+
3+
// StripFields recursively returns a copy of v with any map keys named in
4+
// denylist removed. v is expected to be a JSON-decoded value (map, slice,
5+
// or scalar); other types are returned as-is.
6+
func StripFields(v interface{}, denylist map[string]struct{}) interface{} {
7+
switch val := v.(type) {
8+
case map[string]interface{}:
9+
cleaned := make(map[string]interface{}, len(val))
10+
for key, value := range val {
11+
if _, isStripped := denylist[key]; isStripped {
12+
continue
13+
}
14+
cleaned[key] = StripFields(value, denylist)
15+
}
16+
return cleaned
17+
case []interface{}:
18+
cleaned := make([]interface{}, len(val))
19+
for i, item := range val {
20+
cleaned[i] = StripFields(item, denylist)
21+
}
22+
return cleaned
23+
default:
24+
return val
25+
}
26+
}

testacc/insight_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ func TestInsight_Import(t *testing.T) {
300300
ResourceName: "posthog_insight.test",
301301
ImportState: true,
302302
ImportStateVerify: true,
303-
ImportStateVerifyIgnore: []string{"query_json", "create_in_folder"},
303+
ImportStateVerifyIgnore: []string{"create_in_folder"},
304304
},
305305
},
306306
})

0 commit comments

Comments
 (0)