mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-31 08:44:25 +00:00
Compare commits
14 Commits
v1.0.0
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be38341383 | ||
|
|
fab744b878 | ||
|
|
5ad2bd3a78 | ||
|
|
333fe158e9 | ||
|
|
2a2d351ad4 | ||
|
|
e918c49b84 | ||
|
|
41e4956510 | ||
|
|
8e8c3c6de6 | ||
|
|
aa9b7312f6 | ||
|
|
dca43b0e05 | ||
|
|
6f368bbce5 | ||
|
|
8704cee941 | ||
|
|
4ce5afe0ac | ||
|
|
7b98ea2145 |
@@ -234,35 +234,52 @@ func stripOuterParentheses(s string) string {
|
|||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' {
|
stripped, wasStripped := stripOneMatchingOuterParen(s)
|
||||||
|
if !wasStripped {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
s = stripped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if these parentheses match (i.e., they're the outermost pair)
|
// stripOneOuterParentheses removes only one level of matching outer parentheses from a string
|
||||||
depth := 0
|
// Unlike stripOuterParentheses, this only strips once, preserving nested parentheses
|
||||||
matched := false
|
func stripOneOuterParentheses(s string) string {
|
||||||
for i := 0; i < len(s); i++ {
|
stripped, _ := stripOneMatchingOuterParen(strings.TrimSpace(s))
|
||||||
switch s[i] {
|
return stripped
|
||||||
case '(':
|
}
|
||||||
depth++
|
|
||||||
case ')':
|
// stripOneMatchingOuterParen is a helper that strips one matching pair of outer parentheses
|
||||||
depth--
|
// Returns the stripped string and a boolean indicating if stripping occurred
|
||||||
if depth == 0 && i == len(s)-1 {
|
func stripOneMatchingOuterParen(s string) (string, bool) {
|
||||||
matched = true
|
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' {
|
||||||
} else if depth == 0 {
|
return s, false
|
||||||
// Found a closing paren before the end, so outer parens don't match
|
}
|
||||||
return s
|
|
||||||
}
|
// Check if these parentheses match (i.e., they're the outermost pair)
|
||||||
|
depth := 0
|
||||||
|
matched := false
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch s[i] {
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 && i == len(s)-1 {
|
||||||
|
matched = true
|
||||||
|
} else if depth == 0 {
|
||||||
|
// Found a closing paren before the end, so outer parens don't match
|
||||||
|
return s, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matched {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip the outer parentheses and continue
|
|
||||||
s = strings.TrimSpace(s[1 : len(s)-1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
return s, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the outer parentheses
|
||||||
|
return strings.TrimSpace(s[1 : len(s)-1]), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitByAND splits a WHERE clause by AND operators (case-insensitive)
|
// splitByAND splits a WHERE clause by AND operators (case-insensitive)
|
||||||
@@ -683,8 +700,8 @@ func AddTablePrefixToColumns(where string, tableName string) string {
|
|||||||
// - No valid column reference is found
|
// - No valid column reference is found
|
||||||
// - The column doesn't exist in the table (when validColumns is provided)
|
// - The column doesn't exist in the table (when validColumns is provided)
|
||||||
func addPrefixToSingleCondition(cond string, tableName string, validColumns map[string]bool) string {
|
func addPrefixToSingleCondition(cond string, tableName string, validColumns map[string]bool) string {
|
||||||
// Strip outer grouping parentheses to get to the actual condition
|
// Strip one level of outer grouping parentheses to get to the actual condition
|
||||||
strippedCond := stripOuterParentheses(cond)
|
strippedCond := stripOneOuterParentheses(cond)
|
||||||
|
|
||||||
// Skip SQL literals and trivial conditions (true, false, null, 1=1, etc.)
|
// Skip SQL literals and trivial conditions (true, false, null, 1=1, etc.)
|
||||||
if IsSQLExpression(strippedCond) || IsTrivialCondition(strippedCond) {
|
if IsSQLExpression(strippedCond) || IsTrivialCondition(strippedCond) {
|
||||||
@@ -692,6 +709,34 @@ func addPrefixToSingleCondition(cond string, tableName string, validColumns map[
|
|||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After stripping outer parentheses, check if there are multiple AND-separated conditions
|
||||||
|
// at the top level. If so, split and process each separately to avoid incorrectly
|
||||||
|
// treating "true AND status" as a single column name.
|
||||||
|
subConditions := splitByAND(strippedCond)
|
||||||
|
if len(subConditions) > 1 {
|
||||||
|
// Multiple conditions found - process each separately
|
||||||
|
logger.Debug("Found %d sub-conditions after stripping parentheses, processing separately", len(subConditions))
|
||||||
|
processedConditions := make([]string, 0, len(subConditions))
|
||||||
|
for _, subCond := range subConditions {
|
||||||
|
// Recursively process each sub-condition
|
||||||
|
processed := addPrefixToSingleCondition(subCond, tableName, validColumns)
|
||||||
|
processedConditions = append(processedConditions, processed)
|
||||||
|
}
|
||||||
|
result := strings.Join(processedConditions, " AND ")
|
||||||
|
// Preserve original outer parentheses if they existed
|
||||||
|
if cond != strippedCond {
|
||||||
|
result = "(" + result + ")"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we stripped parentheses and still have more parentheses, recursively process
|
||||||
|
if cond != strippedCond && strings.HasPrefix(strippedCond, "(") && strings.HasSuffix(strippedCond, ")") {
|
||||||
|
// Recursively handle nested parentheses
|
||||||
|
processed := addPrefixToSingleCondition(strippedCond, tableName, validColumns)
|
||||||
|
return "(" + processed + ")"
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the left side of the comparison (before the operator)
|
// Extract the left side of the comparison (before the operator)
|
||||||
columnRef := extractLeftSideOfComparison(strippedCond)
|
columnRef := extractLeftSideOfComparison(strippedCond)
|
||||||
if columnRef == "" {
|
if columnRef == "" {
|
||||||
|
|||||||
@@ -658,3 +658,76 @@ func TestSanitizeWhereClauseWithModel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddTablePrefixToColumns_ComplexConditions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
where string
|
||||||
|
tableName string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Parentheses with true AND condition - should not prefix true",
|
||||||
|
where: "(true AND status = 'active')",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "(true AND mastertask.status = 'active')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parentheses with multiple conditions including true",
|
||||||
|
where: "(true AND status = 'active' AND id > 5)",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "(true AND mastertask.status = 'active' AND mastertask.id > 5)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nested parentheses with true",
|
||||||
|
where: "((true AND status = 'active'))",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "((true AND mastertask.status = 'active'))",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed: false AND valid conditions",
|
||||||
|
where: "(false AND name = 'test')",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "(false AND mastertask.name = 'test')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed: null AND valid conditions",
|
||||||
|
where: "(null AND status = 'active')",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "(null AND mastertask.status = 'active')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple true conditions in parentheses",
|
||||||
|
where: "(true AND true AND status = 'active')",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "(true AND true AND mastertask.status = 'active')",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple true without parens - should not prefix",
|
||||||
|
where: "true",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Simple condition without parens - should prefix",
|
||||||
|
where: "status = 'active'",
|
||||||
|
tableName: "mastertask",
|
||||||
|
expected: "mastertask.status = 'active'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unregistered table with true - should not prefix true",
|
||||||
|
where: "(true AND status = 'active')",
|
||||||
|
tableName: "unregistered_table",
|
||||||
|
expected: "(true AND unregistered_table.status = 'active')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := AddTablePrefixToColumns(tt.where, tt.tableName)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("AddTablePrefixToColumns(%q, %q) = %q; want %q", tt.where, tt.tableName, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
193
pkg/restheadspec/empty_result_test.go
Normal file
193
pkg/restheadspec/empty_result_test.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package restheadspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test that normalizeResultArray returns empty array when no records found without ID
|
||||||
|
func TestNormalizeResultArray_EmptyArrayWhenNoID(t *testing.T) {
|
||||||
|
handler := &Handler{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input interface{}
|
||||||
|
shouldBeEmptyArr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil should return empty array",
|
||||||
|
input: nil,
|
||||||
|
shouldBeEmptyArr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty slice should return empty array",
|
||||||
|
input: []*EmptyTestModel{},
|
||||||
|
shouldBeEmptyArr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single element should return the element",
|
||||||
|
input: []*EmptyTestModel{{ID: 1, Name: "test"}},
|
||||||
|
shouldBeEmptyArr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple elements should return the slice",
|
||||||
|
input: []*EmptyTestModel{
|
||||||
|
{ID: 1, Name: "test1"},
|
||||||
|
{ID: 2, Name: "test2"},
|
||||||
|
},
|
||||||
|
shouldBeEmptyArr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := handler.normalizeResultArray(tt.input)
|
||||||
|
|
||||||
|
// For cases that should return empty array
|
||||||
|
if tt.shouldBeEmptyArr {
|
||||||
|
emptyArr, ok := result.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected empty array []interface{}{}, got %T: %v", result, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(emptyArr) != 0 {
|
||||||
|
t.Errorf("Expected empty array with length 0, got length %d", len(emptyArr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it serializes to [] and not null
|
||||||
|
jsonBytes, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to marshal result: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if string(jsonBytes) != "[]" {
|
||||||
|
t.Errorf("Expected JSON '[]', got '%s'", string(jsonBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that sendFormattedResponse adds X-No-Data-Found header
|
||||||
|
func TestSendFormattedResponse_NoDataFoundHeader(t *testing.T) {
|
||||||
|
handler := &Handler{}
|
||||||
|
|
||||||
|
// Mock ResponseWriter
|
||||||
|
mockWriter := &MockTestResponseWriter{
|
||||||
|
headers: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &common.Metadata{
|
||||||
|
Total: 0,
|
||||||
|
Count: 0,
|
||||||
|
Filtered: 0,
|
||||||
|
Limit: 10,
|
||||||
|
Offset: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
options := ExtendedRequestOptions{
|
||||||
|
RequestOptions: common.RequestOptions{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with empty data
|
||||||
|
emptyData := []interface{}{}
|
||||||
|
handler.sendFormattedResponse(mockWriter, emptyData, metadata, options)
|
||||||
|
|
||||||
|
// Check if X-No-Data-Found header was set
|
||||||
|
if mockWriter.headers["X-No-Data-Found"] != "true" {
|
||||||
|
t.Errorf("Expected X-No-Data-Found header to be 'true', got '%s'", mockWriter.headers["X-No-Data-Found"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the body is an empty array
|
||||||
|
if mockWriter.body == nil {
|
||||||
|
t.Error("Expected body to be set, got nil")
|
||||||
|
} else {
|
||||||
|
bodyBytes, err := json.Marshal(mockWriter.body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to marshal body: %v", err)
|
||||||
|
}
|
||||||
|
// The body should be wrapped in a Response object with "data" field
|
||||||
|
bodyStr := string(bodyBytes)
|
||||||
|
if !strings.Contains(bodyStr, `"data":[]`) && !strings.Contains(bodyStr, `"result":[]`) {
|
||||||
|
t.Errorf("Expected body to contain empty array, got: %s", bodyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that sendResponseWithOptions adds X-No-Data-Found header
|
||||||
|
func TestSendResponseWithOptions_NoDataFoundHeader(t *testing.T) {
|
||||||
|
handler := &Handler{}
|
||||||
|
|
||||||
|
// Mock ResponseWriter
|
||||||
|
mockWriter := &MockTestResponseWriter{
|
||||||
|
headers: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &common.Metadata{}
|
||||||
|
options := &ExtendedRequestOptions{}
|
||||||
|
|
||||||
|
// Test with nil data
|
||||||
|
handler.sendResponseWithOptions(mockWriter, nil, metadata, options)
|
||||||
|
|
||||||
|
// Check if X-No-Data-Found header was set
|
||||||
|
if mockWriter.headers["X-No-Data-Found"] != "true" {
|
||||||
|
t.Errorf("Expected X-No-Data-Found header to be 'true', got '%s'", mockWriter.headers["X-No-Data-Found"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status code is 200
|
||||||
|
if mockWriter.statusCode != 200 {
|
||||||
|
t.Errorf("Expected status code 200, got %d", mockWriter.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the body is an empty array
|
||||||
|
if mockWriter.body == nil {
|
||||||
|
t.Error("Expected body to be set, got nil")
|
||||||
|
} else {
|
||||||
|
bodyBytes, err := json.Marshal(mockWriter.body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to marshal body: %v", err)
|
||||||
|
}
|
||||||
|
bodyStr := string(bodyBytes)
|
||||||
|
if bodyStr != "[]" {
|
||||||
|
t.Errorf("Expected body to be '[]', got: %s", bodyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTestResponseWriter for testing
|
||||||
|
type MockTestResponseWriter struct {
|
||||||
|
headers map[string]string
|
||||||
|
statusCode int
|
||||||
|
body interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTestResponseWriter) SetHeader(key, value string) {
|
||||||
|
m.headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTestResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
m.statusCode = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTestResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTestResponseWriter) WriteJSON(data interface{}) error {
|
||||||
|
m.body = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockTestResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyTestModel for testing
|
||||||
|
type EmptyTestModel struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
@@ -2143,12 +2143,22 @@ func (h *Handler) sendResponse(w common.ResponseWriter, data interface{}, metada
|
|||||||
// sendResponseWithOptions sends a response with optional formatting
|
// sendResponseWithOptions sends a response with optional formatting
|
||||||
func (h *Handler) sendResponseWithOptions(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options *ExtendedRequestOptions) {
|
func (h *Handler) sendResponseWithOptions(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options *ExtendedRequestOptions) {
|
||||||
w.SetHeader("Content-Type", "application/json")
|
w.SetHeader("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Handle nil data - convert to empty array
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = map[string]interface{}{}
|
data = []interface{}{}
|
||||||
w.WriteHeader(http.StatusPartialContent)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate data length after nil conversion
|
||||||
|
dataLen := reflection.Len(data)
|
||||||
|
|
||||||
|
// Add X-No-Data-Found header when no records were found
|
||||||
|
if dataLen == 0 {
|
||||||
|
w.SetHeader("X-No-Data-Found", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
// Normalize single-record arrays to objects if requested
|
// Normalize single-record arrays to objects if requested
|
||||||
if options != nil && options.SingleRecordAsObject {
|
if options != nil && options.SingleRecordAsObject {
|
||||||
data = h.normalizeResultArray(data)
|
data = h.normalizeResultArray(data)
|
||||||
@@ -2165,7 +2175,7 @@ func (h *Handler) sendResponseWithOptions(w common.ResponseWriter, data interfac
|
|||||||
// Returns the single element if data is a slice/array with exactly one element, otherwise returns data unchanged
|
// Returns the single element if data is a slice/array with exactly one element, otherwise returns data unchanged
|
||||||
func (h *Handler) normalizeResultArray(data interface{}) interface{} {
|
func (h *Handler) normalizeResultArray(data interface{}) interface{} {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return map[string]interface{}{}
|
return []interface{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use reflection to check if data is a slice or array
|
// Use reflection to check if data is a slice or array
|
||||||
@@ -2180,15 +2190,15 @@ func (h *Handler) normalizeResultArray(data interface{}) interface{} {
|
|||||||
// Return the single element
|
// Return the single element
|
||||||
return dataValue.Index(0).Interface()
|
return dataValue.Index(0).Interface()
|
||||||
} else if dataValue.Len() == 0 {
|
} else if dataValue.Len() == 0 {
|
||||||
// Return empty object instead of empty array
|
// Keep empty array as empty array, don't convert to empty object
|
||||||
return map[string]interface{}{}
|
return []interface{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataValue.Kind() == reflect.String {
|
if dataValue.Kind() == reflect.String {
|
||||||
str := dataValue.String()
|
str := dataValue.String()
|
||||||
if str == "" || str == "null" {
|
if str == "" || str == "null" {
|
||||||
return map[string]interface{}{}
|
return []interface{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2199,16 +2209,25 @@ func (h *Handler) normalizeResultArray(data interface{}) interface{} {
|
|||||||
func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options ExtendedRequestOptions) {
|
func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{}, metadata *common.Metadata, options ExtendedRequestOptions) {
|
||||||
// Normalize single-record arrays to objects if requested
|
// Normalize single-record arrays to objects if requested
|
||||||
httpStatus := http.StatusOK
|
httpStatus := http.StatusOK
|
||||||
|
|
||||||
|
// Handle nil data - convert to empty array
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = map[string]interface{}{}
|
data = []interface{}{}
|
||||||
httpStatus = http.StatusPartialContent
|
|
||||||
} else {
|
|
||||||
dataLen := reflection.Len(data)
|
|
||||||
if dataLen == 0 {
|
|
||||||
httpStatus = http.StatusPartialContent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate data length after nil conversion
|
||||||
|
// Note: This is done BEFORE normalization because X-No-Data-Found indicates
|
||||||
|
// whether data was found in the database, not the final response format
|
||||||
|
dataLen := reflection.Len(data)
|
||||||
|
|
||||||
|
// Add X-No-Data-Found header when no records were found
|
||||||
|
if dataLen == 0 {
|
||||||
|
w.SetHeader("X-No-Data-Found", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply normalization after header is set
|
||||||
|
// normalizeResultArray may convert single-element arrays to objects,
|
||||||
|
// but the X-No-Data-Found header reflects the original query result
|
||||||
if options.SingleRecordAsObject {
|
if options.SingleRecordAsObject {
|
||||||
data = h.normalizeResultArray(data)
|
data = h.normalizeResultArray(data)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user