Skip to content

Commit f587e92

Browse files
authored
Merge pull request #62 from amichaelyu/param_fix
feat: enhance filter builder to support multiple filters on the same …
2 parents 9e475dc + 4e736fa commit f587e92

File tree

2 files changed

+158
-47
lines changed

2 files changed

+158
-47
lines changed

filterbuilder.go

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ func (f *FilterBuilder) ExecuteToWithContext(ctx context.Context, to interface{}
5858

5959
var filterOperators = []string{"eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike", "is", "in", "cs", "cd", "sl", "sr", "nxl", "nxr", "adj", "ov", "fts", "plfts", "phfts", "wfts"}
6060

61+
// appendFilter is a helper method that appends a filter to existing filters on a column
62+
func (f *FilterBuilder) appendFilter(column, filterValue string) *FilterBuilder {
63+
if existing, ok := f.params[column]; ok && existing != "" {
64+
// If a filter already exists for this column, combine with 'and'
65+
f.params["and"] = fmt.Sprintf("(%s.%s,%s.%s)", column, existing, column, filterValue)
66+
delete(f.params, column)
67+
} else if existingAnd, ok := f.params["and"]; ok {
68+
// If an 'and' parameter already exists, append to it
69+
f.params["and"] = strings.TrimSuffix(existingAnd, ")") + "," + column + "." + filterValue + ")"
70+
} else {
71+
f.params[column] = filterValue
72+
}
73+
return f
74+
}
75+
6176
func isOperator(value string) bool {
6277
for _, operator := range filterOperators {
6378
if value == operator {
@@ -74,8 +89,7 @@ func (f *FilterBuilder) Filter(column, operator, value string) *FilterBuilder {
7489
f.client.ClientError = fmt.Errorf("invalid filter operator")
7590
return f
7691
}
77-
f.params[column] = fmt.Sprintf("%s.%s", operator, value)
78-
return f
92+
return f.appendFilter(column, fmt.Sprintf("%s.%s", operator, value))
7993
}
8094

8195
func (f *FilterBuilder) And(filters, foreignTable string) *FilterBuilder {
@@ -100,60 +114,50 @@ func (f *FilterBuilder) Not(column, operator, value string) *FilterBuilder {
100114
if !isOperator(operator) {
101115
return f
102116
}
103-
f.params[column] = fmt.Sprintf("not.%s.%s", operator, value)
104-
return f
117+
return f.Filter(column, "not."+operator, value)
105118
}
106119

107120
func (f *FilterBuilder) Match(userQuery map[string]string) *FilterBuilder {
108121
for key, value := range userQuery {
109-
f.params[key] = "eq." + value
122+
f.Filter(key, "eq", value)
110123
}
111124
return f
112125
}
113126

114127
func (f *FilterBuilder) Eq(column, value string) *FilterBuilder {
115-
f.params[column] = "eq." + value
116-
return f
128+
return f.Filter(column, "eq", value)
117129
}
118130

119131
func (f *FilterBuilder) Neq(column, value string) *FilterBuilder {
120-
f.params[column] = "neq." + value
121-
return f
132+
return f.Filter(column, "neq", value)
122133
}
123134

124135
func (f *FilterBuilder) Gt(column, value string) *FilterBuilder {
125-
f.params[column] = "gt." + value
126-
return f
136+
return f.Filter(column, "gt", value)
127137
}
128138

129139
func (f *FilterBuilder) Gte(column, value string) *FilterBuilder {
130-
f.params[column] = "gte." + value
131-
return f
140+
return f.Filter(column, "gte", value)
132141
}
133142

134143
func (f *FilterBuilder) Lt(column, value string) *FilterBuilder {
135-
f.params[column] = "lt." + value
136-
return f
144+
return f.Filter(column, "lt", value)
137145
}
138146

139147
func (f *FilterBuilder) Lte(column, value string) *FilterBuilder {
140-
f.params[column] = "lte." + value
141-
return f
148+
return f.Filter(column, "lte", value)
142149
}
143150

144151
func (f *FilterBuilder) Like(column, value string) *FilterBuilder {
145-
f.params[column] = "like." + value
146-
return f
152+
return f.Filter(column, "like", value)
147153
}
148154

149155
func (f *FilterBuilder) Ilike(column, value string) *FilterBuilder {
150-
f.params[column] = "ilike." + value
151-
return f
156+
return f.Filter(column, "ilike", value)
152157
}
153158

154159
func (f *FilterBuilder) Is(column, value string) *FilterBuilder {
155-
f.params[column] = "is." + value
156-
return f
160+
return f.Filter(column, "is", value)
157161
}
158162

159163
func (f *FilterBuilder) In(column string, values []string) *FilterBuilder {
@@ -167,8 +171,7 @@ func (f *FilterBuilder) In(column string, values []string) *FilterBuilder {
167171
cleanedValues = append(cleanedValues, value)
168172
}
169173
}
170-
f.params[column] = fmt.Sprintf("in.(%s)", strings.Join(cleanedValues, ","))
171-
return f
174+
return f.appendFilter(column, fmt.Sprintf("in.(%s)", strings.Join(cleanedValues, ",")))
172175
}
173176

174177
func (f *FilterBuilder) Contains(column string, value []string) *FilterBuilder {
@@ -179,8 +182,7 @@ func (f *FilterBuilder) Contains(column string, value []string) *FilterBuilder {
179182

180183
valueString := fmt.Sprintf("{%s}", strings.Join(newValue, ","))
181184

182-
f.params[column] = "cs." + valueString
183-
return f
185+
return f.appendFilter(column, "cs."+valueString)
184186
}
185187

186188
func (f *FilterBuilder) ContainedBy(column string, value []string) *FilterBuilder {
@@ -191,51 +193,45 @@ func (f *FilterBuilder) ContainedBy(column string, value []string) *FilterBuilde
191193

192194
valueString := fmt.Sprintf("{%s}", strings.Join(newValue, ","))
193195

194-
f.params[column] = "cd." + valueString
195-
return f
196+
return f.appendFilter(column, "cd."+valueString)
196197
}
197198

198199
func (f *FilterBuilder) ContainsObject(column string, value interface{}) *FilterBuilder {
199200
sum, err := json.Marshal(value)
200201
if err != nil {
201202
f.client.ClientError = err
203+
return f
202204
}
203-
f.params[column] = "cs." + string(sum)
204-
return f
205+
return f.appendFilter(column, "cs."+string(sum))
205206
}
206207

207208
func (f *FilterBuilder) ContainedByObject(column string, value interface{}) *FilterBuilder {
208209
sum, err := json.Marshal(value)
209210
if err != nil {
210211
f.client.ClientError = err
212+
return f
211213
}
212-
f.params[column] = "cs." + string(sum)
213-
return f
214+
return f.appendFilter(column, "cd."+string(sum))
214215
}
215216

216217
func (f *FilterBuilder) RangeLt(column, value string) *FilterBuilder {
217-
f.params[column] = "sl." + value
218-
return f
218+
return f.appendFilter(column, "sl."+value)
219219
}
220220

221221
func (f *FilterBuilder) RangeGt(column, value string) *FilterBuilder {
222-
f.params[column] = "sr." + value
223-
return f
222+
return f.appendFilter(column, "sr."+value)
224223
}
225224

226225
func (f *FilterBuilder) RangeGte(column, value string) *FilterBuilder {
227-
f.params[column] = "nxl." + value
228-
return f
226+
return f.appendFilter(column, "nxl."+value)
229227
}
230228

231229
func (f *FilterBuilder) RangeLte(column, value string) *FilterBuilder {
232-
f.params[column] = "nxr." + value
233-
return f
230+
return f.appendFilter(column, "nxr."+value)
234231
}
235232

236233
func (f *FilterBuilder) RangeAdjacent(column, value string) *FilterBuilder {
237-
f.params[column] = "adj." + value
238-
return f
234+
return f.appendFilter(column, "adj."+value)
239235
}
240236

241237
func (f *FilterBuilder) Overlaps(column string, value []string) *FilterBuilder {
@@ -245,8 +241,7 @@ func (f *FilterBuilder) Overlaps(column string, value []string) *FilterBuilder {
245241
}
246242

247243
valueString := fmt.Sprintf("{%s}", strings.Join(newValue, ","))
248-
f.params[column] = "ov." + valueString
249-
return f
244+
return f.appendFilter(column, "ov."+valueString)
250245
}
251246

252247
// TextSearch performs a full-text search filter. For more information, see
@@ -268,8 +263,7 @@ func (f *FilterBuilder) TextSearch(column, userQuery, config, tsType string) *Fi
268263
if config != "" {
269264
configPart = fmt.Sprintf("(%s)", config)
270265
}
271-
f.params[column] = typePart + "fts" + configPart + "." + userQuery
272-
return f
266+
return f.appendFilter(column, typePart+"fts"+configPart+"."+userQuery)
273267
}
274268

275269
// OrderOpts describes the options to be provided to Order.

filterbuilder_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,120 @@ func TestFilterBuilder_Single(t *testing.T) {
263263
assert.Error(err)
264264
})
265265
}
266+
267+
func TestFilterAppend(t *testing.T) {
268+
tests := []struct {
269+
name string
270+
build func(*FilterBuilder) *FilterBuilder
271+
expected map[string]string
272+
}{
273+
{
274+
name: "Single filter on column",
275+
build: func(fb *FilterBuilder) *FilterBuilder {
276+
return fb.Eq("age", "25")
277+
},
278+
expected: map[string]string{
279+
"age": "eq.25",
280+
},
281+
},
282+
{
283+
name: "Multiple filters on same column",
284+
build: func(fb *FilterBuilder) *FilterBuilder {
285+
return fb.Gte("age", "25").Lte("age", "35")
286+
},
287+
expected: map[string]string{
288+
"and": "(age.gte.25,age.lte.35)",
289+
},
290+
},
291+
{
292+
name: "Three filters on same column",
293+
build: func(fb *FilterBuilder) *FilterBuilder {
294+
return fb.Gte("age", "25").Lte("age", "35").Neq("age", "30")
295+
},
296+
expected: map[string]string{
297+
"and": "(age.gte.25,age.lte.35,age.neq.30)",
298+
},
299+
},
300+
{
301+
name: "Multiple columns with multiple filters",
302+
build: func(fb *FilterBuilder) *FilterBuilder {
303+
return fb.Eq("status", "active").Gte("age", "25").Lte("age", "35")
304+
},
305+
expected: map[string]string{
306+
"status": "eq.active",
307+
"and": "(age.gte.25,age.lte.35)",
308+
},
309+
},
310+
{
311+
name: "In filter followed by another filter",
312+
build: func(fb *FilterBuilder) *FilterBuilder {
313+
return fb.In("id", []string{"1", "2", "3"}).Eq("id", "4")
314+
},
315+
expected: map[string]string{
316+
"and": "(id.in.(1,2,3),id.eq.4)",
317+
},
318+
},
319+
{
320+
name: "Contains filter followed by another filter",
321+
build: func(fb *FilterBuilder) *FilterBuilder {
322+
return fb.Contains("tags", []string{"golang", "postgres"}).Overlaps("tags", []string{"javascript"})
323+
},
324+
expected: map[string]string{
325+
"and": "(tags.cs.{\"golang\",\"postgres\"},tags.ov.{\"javascript\"})",
326+
},
327+
},
328+
{
329+
name: "Text search followed by Like filter",
330+
build: func(fb *FilterBuilder) *FilterBuilder {
331+
return fb.TextSearch("title", "golang", "", "plain").Like("title", "%tutorial%")
332+
},
333+
expected: map[string]string{
334+
"and": "(title.plfts.golang,title.like.%tutorial%)",
335+
},
336+
},
337+
{
338+
name: "Range filters on same column",
339+
build: func(fb *FilterBuilder) *FilterBuilder {
340+
return fb.RangeGt("period", "[2022-01-01,2022-12-31]").RangeLt("period", "[2023-01-01,2023-12-31]")
341+
},
342+
expected: map[string]string{
343+
"and": "(period.sr.[2022-01-01,2022-12-31],period.sl.[2023-01-01,2023-12-31])",
344+
},
345+
},
346+
}
347+
348+
for _, tt := range tests {
349+
t.Run(tt.name, func(t *testing.T) {
350+
client := NewClient("http://localhost:3000", "", nil)
351+
fb := &FilterBuilder{
352+
client: client,
353+
method: "GET",
354+
tableName: "test",
355+
headers: make(map[string]string),
356+
params: make(map[string]string),
357+
}
358+
359+
result := tt.build(fb)
360+
361+
// Check that we got the expected params
362+
if len(result.params) != len(tt.expected) {
363+
t.Errorf("Expected %d params, got %d", len(tt.expected), len(result.params))
364+
}
365+
366+
for key, expectedValue := range tt.expected {
367+
if actualValue, ok := result.params[key]; !ok {
368+
t.Errorf("Expected param %s not found", key)
369+
} else if actualValue != expectedValue {
370+
t.Errorf("Param %s: expected %s, got %s", key, expectedValue, actualValue)
371+
}
372+
}
373+
374+
// Check that no unexpected params exist
375+
for key := range result.params {
376+
if _, ok := tt.expected[key]; !ok {
377+
t.Errorf("Unexpected param %s found", key)
378+
}
379+
}
380+
})
381+
}
382+
}

0 commit comments

Comments
 (0)