Skip to content

Commit 76a01b7

Browse files
committed
rest: add jsonb containment operators @> and <@
Signed-off-by: hmoazzem <[email protected]>
1 parent 2ee918c commit 76a01b7

File tree

5 files changed

+80
-22
lines changed

5 files changed

+80
-22
lines changed

pkg/httputil/context.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,13 @@ func BindOrError(r *http.Request, w http.ResponseWriter, dst any) error {
6161
}
6262

6363
// JSON writes a JSON response with the given status code and data.
64-
func JSON(w http.ResponseWriter, statusCode int, data any) {
64+
func JSON(w http.ResponseWriter, statusCode int, v any) {
6565
w.Header().Set("Content-Type", "application/json")
6666
w.WriteHeader(statusCode)
67-
if err := json.NewEncoder(w).Encode(data); err != nil {
68-
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
69-
}
67+
68+
encoder := json.NewEncoder(w)
69+
encoder.SetIndent("", " ") // double-spaced pretty json. should take from config
70+
encoder.Encode(v)
7071
}
7172

7273
// Text writes a plain text response with the given status code and text content.

pkg/httputil/router.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111
"sync"
1212

13+
"slices"
14+
1315
"github.com/edgeflare/pgo/pkg/util"
1416
)
1517

@@ -95,7 +97,7 @@ func (r *Router) Group(prefix string) *Router {
9597
defer r.mu.RUnlock()
9698
return &Router{
9799
mux: r.mux,
98-
middleware: append([]Middleware{}, r.middleware...),
100+
middleware: slices.Clone(r.middleware),
99101
server: r.server,
100102
prefix: r.prefix + prefix,
101103
}

pkg/rest/doc.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,24 @@
88
//
99
// Query parameters control filtering, pagination, and ordering:
1010
//
11-
// Parameter | Description
12-
// ------------------|------------------------------------------------
13-
// ?select=col1,col2 | Select specific columns
14-
// ?order=col.desc | Order results (supports nullsfirst/nullslast)
11+
// Parameter | Description
12+
// --------------------|------------------------------------------------
13+
// ?select=col1,col2 | Select specific columns
14+
// ?order=col.desc | Order results (supports nullsfirst/nullslast)
1515
// ?order=similarity(col, 'search string') | Order by similarity (requires pg_trgm extension)
16-
// ?limit=100 | Limit number of results (default: 100)
17-
// ?offset=0 | Pagination offset (default: 0)
18-
// ?col=eq.val | Filter by column equality
19-
// ?col=gt.val | Filter with greater than comparison
20-
// ?col=lt.val | Filter with less than comparison
21-
// ?col=gte.val | Filter with greater than or equal comparison
22-
// ?col=lte.val | Filter with less than or equal comparison
23-
// ?col=like.val | Filter with pattern matching
24-
// ?col=in.(a,b,c) | Filter with value lists
25-
// ?col=is.null | Filter for null values
26-
// ?or=(a.eq.x,b.lt.y) | Combine filters with logical operators
16+
// ?limit=100 | Limit number of results (default: 100)
17+
// ?offset=0 | Pagination offset (default: 0)
18+
// ?col=eq.val | Filter by column equality
19+
// ?col=gt.val | Filter with greater than comparison
20+
// ?col=lt.val | Filter with less than comparison
21+
// ?col=gte.val | Filter with greater than or equal comparison
22+
// ?col=lte.val | Filter with less than or equal comparison
23+
// ?col=like.val | Filter with pattern matching
24+
// ?col=in.a,b,c | Filter with value lists
25+
// ?col=is.null | Filter for null values
26+
// ?or=(a.eq.x,b.lt.y) | Combine filters with logical operators
27+
// ?col=cs.example,new | contains (@>) The column contains all these values. JSON object can be passed with urlencoded string
28+
// ?col=cd.1,2,3. | is contained in (<@) The column’s values are all within this set
2729
//
2830
// HTTP headers control response format for POST/PATCH/DELETE operations:
2931
//

pkg/rest/query.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,21 @@ func parseFilterParam(value string) []FilterParam {
100100
"fts": "@@",
101101
"plfts": "@@",
102102
"phfts": "@@",
103+
"cs": "@>",
104+
"cd": "<@",
105+
}
106+
107+
// Skip split by comma if this is a containment operator
108+
// https://www.postgresql.org/docs/current/datatype-json.html#JSON-CONTAINMENT
109+
hasContainmentOperator := strings.HasPrefix(value, "cs.") || strings.HasPrefix(value, "cd.")
110+
111+
var orParts []string
112+
if hasContainmentOperator {
113+
orParts = []string{value}
114+
} else {
115+
orParts = strings.Split(value, ",")
103116
}
104117

105-
// Split by commas for OR conditions
106-
orParts := strings.Split(value, ",")
107118
for _, orPart := range orParts {
108119
// Default to equality if no operator is specified
109120
operator := "="
@@ -126,6 +137,9 @@ func parseFilterParam(value string) []FilterParam {
126137
} else if val == "null" && operator == "!=" {
127138
operator = "IS NOT"
128139
filterValue = nil
140+
} else if operator == "@>" || operator == "<@" {
141+
// Handle array/JSONB containment operators
142+
filterValue = convertToPostgresArrayOrJSON(val)
129143
} else {
130144
filterValue = val
131145
}

pkg/rest/util.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package rest
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
func convertToPostgresArrayOrJSON(val string) string {
9+
val = strings.TrimSpace(val)
10+
11+
// check if it's already JSON format (contains quotes around elements)
12+
// e.g., {"NVMe"} or ["value"] or {"key": "value"}
13+
if strings.Contains(val, "\"") || strings.HasPrefix(val, "[") {
14+
// already JSON format, return as-is for JSONB columns
15+
return val
16+
}
17+
18+
// otherwise, treat as PostgREST array syntax {a,b,c}
19+
// remove outer braces if present
20+
if strings.HasPrefix(val, "{") && strings.HasSuffix(val, "}") {
21+
val = val[1 : len(val)-1]
22+
}
23+
24+
// split by comma and trim whitespace
25+
parts := strings.Split(val, ",")
26+
quotedParts := make([]string, 0, len(parts))
27+
for _, part := range parts {
28+
part = strings.TrimSpace(part)
29+
if part != "" {
30+
// escape any quotes in the value
31+
part = strings.ReplaceAll(part, "\"", "\\\"")
32+
quotedParts = append(quotedParts, fmt.Sprintf("\"%s\"", part))
33+
}
34+
}
35+
36+
// return as PostgreSQL array literal for array columns
37+
// or JSONB array for JSONB columns
38+
return fmt.Sprintf("{%s}", strings.Join(quotedParts, ","))
39+
}

0 commit comments

Comments
 (0)