Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/cursor-pagination-body-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"grafana-infinity-datasource": patch
---

Fix cursor pagination for POST APIs that expect the next page token inside the JSON body. The `body_json` pagination param type is now implemented (cursor value is injected into the request JSON body), exposed in the query editor, and cursor pagination no longer errors when the final page omits the cursor field.
19 changes: 18 additions & 1 deletion pkg/infinity/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ func ApplyPaginationItemToQuery(query models.Query, fieldType models.PaginationP
query.URLOptions.Headers = append(query.URLOptions.Headers, field)
case models.PaginationParamTypeBodyData:
query.URLOptions.BodyForm = append(query.URLOptions.BodyForm, field)
case models.PaginationParamTypeBodyJson:
body := map[string]any{}
if trimmed := strings.TrimSpace(query.URLOptions.Body); trimmed != "" {
if err := json.Unmarshal([]byte(trimmed), &body); err != nil {
// Body isn't a valid JSON object; leave it untouched rather than
// overwriting the user's payload with just the cursor field.
return query
}
}
body[fieldName] = fieldValue
if b, err := json.Marshal(body); err == nil {
query.URLOptions.Body = string(b)
}
case models.PaginationParamTypeReplace:
fieldNameUpdated := fmt.Sprintf(`${__pagination.%s}`, fieldName)
query.URL = strings.ReplaceAll(query.URL, fieldNameUpdated, field.Value)
Expand Down Expand Up @@ -229,7 +242,11 @@ func GetFrameForURLSourcesWithPostProcessing(ctx context.Context, pCtx *backend.
}
cursor, err = jsonframer.GetRootData(string(body), query.PageParamCursorFieldExtractionPath, framerType)
if err != nil {
return frame, cursor, backend.PluginError(errors.New("error while extracting the cursor value"))
// Many cursor-based APIs omit the cursor field entirely on the last
// page (instead of returning an empty value). In that case the
// extraction yields no result, which we treat as "no more pages"
// rather than a fatal error so pagination stops gracefully.
return frame, "", nil
}
}
return frame, cursor, nil
Expand Down
32 changes: 30 additions & 2 deletions pkg/infinity/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,42 @@ func TestApplyPaginationItemToQuery(t *testing.T) {
}
})
t.Run(string(models.PaginationParamTypeBodyJson), func(t *testing.T) {
t.Skip() // TODO: NOT IMPLEMENTED YET
tests := []struct {
name string
query models.Query
fieldName string
fieldValue string
want models.Query
}{}
}{
{
name: "should inject pagination cursor into empty body",
query: models.Query{URLOptions: models.URLOptions{Body: ""}},
fieldName: "nextPageToken",
fieldValue: "abc123",
want: models.Query{URLOptions: models.URLOptions{Body: `{"nextPageToken":"abc123"}`}},
},
{
name: "should inject pagination cursor into existing JSON body",
query: models.Query{URLOptions: models.URLOptions{Body: `{"filter":"active"}`}},
fieldName: "nextPageToken",
fieldValue: "page2",
want: models.Query{URLOptions: models.URLOptions{Body: `{"filter":"active","nextPageToken":"page2"}`}},
},
{
name: "should overwrite existing cursor key in JSON body",
query: models.Query{URLOptions: models.URLOptions{Body: `{"nextPageToken":"old","filter":"active"}`}},
fieldName: "nextPageToken",
fieldValue: "new",
want: models.Query{URLOptions: models.URLOptions{Body: `{"filter":"active","nextPageToken":"new"}`}},
},
{
name: "should not overwrite a non-JSON body",
query: models.Query{URLOptions: models.URLOptions{Body: `not a json body`}},
fieldName: "nextPageToken",
fieldValue: "abc123",
want: models.Query{URLOptions: models.URLOptions{Body: `not a json body`}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ApplyPaginationItemToQuery(tt.query, models.PaginationParamTypeBodyJson, tt.fieldName, tt.fieldValue)
Expand Down
2 changes: 1 addition & 1 deletion pkg/models/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const (
PaginationParamTypeQuery PaginationParamType = "query"
PaginationParamTypeHeader PaginationParamType = "header"
PaginationParamTypeBodyData PaginationParamType = "body_data"
PaginationParamTypeBodyJson PaginationParamType = "body_json" // TODO: NOT IMPLEMENTED YET
PaginationParamTypeBodyJson PaginationParamType = "body_json"
PaginationParamTypeReplace PaginationParamType = "replace"
)

Expand Down
2 changes: 1 addition & 1 deletion src/editors/query/query.pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const paginationParamTypes: Array<ComboboxOption<PaginationParamType>> = [
{ value: 'query', label: 'Query param' },
{ value: 'header', label: 'Header' },
{ value: 'body_data', label: 'Body form' },
// { value: 'body_json', label: 'Body JSON' },
{ value: 'body_json', label: 'Body JSON' },
{ value: 'replace', label: 'Replace URL' },
];

Expand Down