diff --git a/pkg/common/reflection_utils.go b/pkg/common/reflection_utils.go new file mode 100644 index 0000000..712c046 --- /dev/null +++ b/pkg/common/reflection_utils.go @@ -0,0 +1,13 @@ +package common + +import "reflect" + +func Len(v any) int { + val := reflect.ValueOf(v) + switch val.Kind() { + case reflect.Slice, reflect.Array, reflect.Map, reflect.String, reflect.Chan: + return val.Len() + default: + return 0 + } +} diff --git a/pkg/common/types.go b/pkg/common/types.go index a5c6d70..2642291 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -18,6 +18,11 @@ type RequestOptions struct { CustomOperators []CustomOperator `json:"customOperators"` ComputedColumns []ComputedColumn `json:"computedColumns"` Parameters []Parameter `json:"parameters"` + + // Cursor pagination + CursorForward string `json:"cursor_forward"` + CursorBackward string `json:"cursor_backward"` + FetchRowNumber *string `json:"fetch_row_number"` } type Parameter struct { @@ -68,6 +73,7 @@ type Response struct { type Metadata struct { Total int64 `json:"total"` + Count int64 `json:"count"` Filtered int64 `json:"filtered"` Limit int `json:"limit"` Offset int `json:"offset"` diff --git a/pkg/restheadspec/cursor.go b/pkg/restheadspec/cursor.go index 6998217..7e4b5c0 100644 --- a/pkg/restheadspec/cursor.go +++ b/pkg/restheadspec/cursor.go @@ -31,7 +31,9 @@ func (opts *ExtendedRequestOptions) GetCursorFilter( modelColumns []string, // optional: for validation expandJoins map[string]string, // optional: alias → JOIN SQL ) (string, error) { - + if strings.Contains(tableName, ".") { + tableName = strings.SplitN(tableName, ".", 2)[1] + } // --------------------------------------------------------------------- // // 1. Determine active cursor // --------------------------------------------------------------------- // @@ -137,11 +139,11 @@ func (opts *ExtendedRequestOptions) GetCursorFilter( // ------------------------------------------------------------------------- // // Helper: get active cursor (forward or backward) func (opts *ExtendedRequestOptions) getActiveCursor() (id string, direction CursorDirection) { - if opts.CursorForward != "" { - return opts.CursorForward, CursorForward + if opts.RequestOptions.CursorForward != "" { + return opts.RequestOptions.CursorForward, CursorForward } - if opts.CursorBackward != "" { - return opts.CursorBackward, CursorBackward + if opts.RequestOptions.CursorBackward != "" { + return opts.RequestOptions.CursorBackward, CursorBackward } return "", 0 } diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index df8cdd1..d45f3de 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -339,7 +339,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st } // Apply cursor-based pagination - if len(options.CursorForward) > 0 || len(options.CursorBackward) > 0 { + if len(options.RequestOptions.CursorForward) > 0 || len(options.RequestOptions.CursorBackward) > 0 { logger.Debug("Applying cursor pagination") // Get primary key name @@ -389,6 +389,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st metadata := &common.Metadata{ Total: int64(total), + Count: int64(common.Len(modelPtr)), Filtered: int64(total), Limit: limit, Offset: offset, @@ -937,7 +938,12 @@ func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{ if options.CleanJSON { data = h.cleanJSON(data) } + w.SetHeader("Content-Type", "application/json") + w.SetHeader("Content-Range", fmt.Sprintf("%d-%d/%d", metadata.Offset, int64(metadata.Offset)+metadata.Count, metadata.Filtered)) + w.SetHeader("X-Api-Range-Total", fmt.Sprintf("%d", metadata.Filtered)) + w.SetHeader("X-Api-Range-Size", fmt.Sprintf("%d", metadata.Count)) + // Format response based on response format option switch options.ResponseFormat { case "simple": diff --git a/pkg/restheadspec/headers.go b/pkg/restheadspec/headers.go index 34f438f..bc2e6a3 100644 --- a/pkg/restheadspec/headers.go +++ b/pkg/restheadspec/headers.go @@ -28,23 +28,18 @@ type ExtendedRequestOptions struct { Expand []ExpandOption // Advanced features - AdvancedSQL map[string]string // Column -> SQL expression - ComputedQL map[string]string // Column -> CQL expression - Distinct bool - SkipCount bool - SkipCache bool - FetchRowNumber *string - PKRow *string + AdvancedSQL map[string]string // Column -> SQL expression + ComputedQL map[string]string // Column -> CQL expression + Distinct bool + SkipCount bool + SkipCache bool + PKRow *string // Response format ResponseFormat string // "simple", "detail", "syncfusion" // Transaction AtomicTransaction bool - - // Cursor pagination - CursorForward string - CursorBackward string } // ExpandOption represents a relation expansion configuration @@ -171,9 +166,9 @@ func (h *Handler) parseOptionsFromHeaders(r common.Request) ExtendedRequestOptio options.Offset = &offset } case strings.HasPrefix(normalizedKey, "x-cursor-forward"): - options.CursorForward = decodedValue + options.RequestOptions.CursorForward = decodedValue case strings.HasPrefix(normalizedKey, "x-cursor-backward"): - options.CursorBackward = decodedValue + options.RequestOptions.CursorBackward = decodedValue // Advanced Features case strings.HasPrefix(normalizedKey, "x-advsql-"): @@ -189,7 +184,7 @@ func (h *Handler) parseOptionsFromHeaders(r common.Request) ExtendedRequestOptio case strings.HasPrefix(normalizedKey, "x-skipcache"): options.SkipCache = strings.ToLower(decodedValue) == "true" case strings.HasPrefix(normalizedKey, "x-fetch-rownumber"): - options.FetchRowNumber = &decodedValue + options.RequestOptions.FetchRowNumber = &decodedValue case strings.HasPrefix(normalizedKey, "x-pkrow"): options.PKRow = &decodedValue