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
42 changes: 31 additions & 11 deletions internal/store/postgres/org_users_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,31 +325,51 @@ func (r OrgUsersRepository) buildHasAnyRoleSubquery(orgID string) *goqu.SelectDa
}

func (r OrgUsersRepository) buildNonRoleFilterCondition(filter rql.Filter) (goqu.Expression, error) {
//this list will always be a subset of all RQL supported operators
supportedStringOperators := []string{"eq", "neq", "like", "in", "notin", "notlike", "empty", "notempty"}
supportedDateTimeOperators := []string{"eq", "neq", "gt", "lt", "gte", "lte"}

if !slices.Contains(supportedStringOperators, filter.Operator) && !slices.Contains(supportedDateTimeOperators, filter.Operator) {
return nil, wrapBadOperatorError(filter.Operator, filter.Name)
// Route by the column's declared datatype so that string-only operators
// (like/ilike/in/empty) are rejected on the datetime column org_joined_at.
datatype, err := rql.GetDataTypeOfField(filter.Name, svc.AggregatedUser{})
if err != nil {
return nil, err
}

columnName := r.getNonRoleColumnName(filter.Name)

if datatype == "datetime" {
supportedDateTimeOperators := []string{"eq", "neq", "gt", "lt", "gte", "lte"}
if !slices.Contains(supportedDateTimeOperators, filter.Operator) {
return nil, wrapBadOperatorError(filter.Operator, filter.Name)
}
return goqu.Ex{columnName: goqu.Op{filter.Operator: filter.Value.(string)}}, nil
}

// string columns
supportedStringOperators := []string{"eq", "neq", "like", "ilike", "in", "notin", "notlike", "notilike", "empty", "notempty"}
if !slices.Contains(supportedStringOperators, filter.Operator) {
return nil, wrapBadOperatorError(filter.Operator, filter.Name)
}

switch filter.Operator {
case "empty":
return goqu.Or(goqu.I(columnName).IsNull(), goqu.I(columnName).Eq("")), nil
case "notempty":
return goqu.And(goqu.I(columnName).IsNotNull(), goqu.I(columnName).Neq("")), nil
case "in", "notin":
return goqu.Ex{columnName: goqu.Op{filter.Operator: strings.Split(filter.Value.(string), ",")}}, nil
// like/notlike are case-sensitive (SQL LIKE); ilike/notilike are case-insensitive
// (SQL ILIKE). The caller owns the wildcards (e.g. "%foo%") in every case — the
// DataTable filter sends them pre-wrapped.
case "like":
searchPattern := "%" + filter.Value.(string) + "%"
return goqu.Cast(goqu.I(columnName), "TEXT").ILike(searchPattern), nil
return goqu.Cast(goqu.I(columnName), "TEXT").Like(filter.Value.(string)), nil
case "notlike":
searchPattern := "%" + filter.Value.(string) + "%"
return goqu.Cast(goqu.I(columnName), "TEXT").NotILike(searchPattern), nil
default: // eq, neq
return goqu.Cast(goqu.I(columnName), "TEXT").NotLike(filter.Value.(string)), nil
case "ilike":
return goqu.Cast(goqu.I(columnName), "TEXT").ILike(filter.Value.(string)), nil
case "notilike":
return goqu.Cast(goqu.I(columnName), "TEXT").NotILike(filter.Value.(string)), nil
case "eq", "neq":
return goqu.Ex{columnName: goqu.Op{filter.Operator: filter.Value.(string)}}, nil
default:
return nil, wrapBadOperatorError(filter.Operator, filter.Name)
}
}

Expand Down
55 changes: 51 additions & 4 deletions internal/store/postgres/org_users_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,39 @@ func TestOrgUsersRepository_BuildNonRoleFilterCondition(t *testing.T) {
filter: rql.Filter{
Name: "name",
Operator: "like",
Value: "john",
Value: "%john%",
},
wantSQL: `(CAST("users"."name" AS TEXT) ILIKE $1)`,
wantSQL: `(CAST("users"."name" AS TEXT) LIKE $1)`,
wantArgs: []interface{}{"%john%"},
},
{
name: "notlike operator",
filter: rql.Filter{
Name: "name",
Operator: "notlike",
Value: "john",
Value: "%john%",
},
wantSQL: `(CAST("users"."name" AS TEXT) NOT ILIKE $1)`,
wantSQL: `(CAST("users"."name" AS TEXT) NOT LIKE $1)`,
wantArgs: []interface{}{"%john%"},
},
{
name: "ilike operator",
filter: rql.Filter{
Name: "title",
Operator: "ilike",
Value: "%john%",
},
wantSQL: `(CAST("users"."title" AS TEXT) ILIKE $1)`,
wantArgs: []interface{}{"%john%"},
},
{
name: "notilike operator",
filter: rql.Filter{
Name: "title",
Operator: "notilike",
Value: "%john%",
},
wantSQL: `(CAST("users"."title" AS TEXT) NOT ILIKE $1)`,
wantArgs: []interface{}{"%john%"},
},
{
Expand All @@ -164,6 +184,33 @@ func TestOrgUsersRepository_BuildNonRoleFilterCondition(t *testing.T) {
wantSQL: `(("users"."title" IS NULL) OR ("users"."title" = $1))`,
wantArgs: []interface{}{""},
},
{
name: "datetime gte operator",
filter: rql.Filter{
Name: "org_joined_at",
Operator: "gte",
Value: "2024-01-01T00:00:00Z",
},
wantSQL: `("policies"."created_at" >= $1)`,
wantArgs: []interface{}{"2024-01-01T00:00:00Z"},
},
{
name: "ilike operator not allowed on datetime column",
filter: rql.Filter{
Name: "org_joined_at",
Operator: "ilike",
Value: "%2024%",
},
wantErr: true,
},
{
name: "empty operator not allowed on datetime column",
filter: rql.Filter{
Name: "org_joined_at",
Operator: "empty",
},
wantErr: true,
},
{
name: "invalid operator",
filter: rql.Filter{
Expand Down
Loading