diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 87645e0..158d9d5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -182,6 +182,20 @@ paths: in: query name: pageSize type: integer + - description: Filter by name. Asterisks can be used to represent any character(s) + in: query + name: name + type: string + - description: Filter by phone number. Asterisks can be used to represent any + character(s) + in: query + name: phone + type: string + - default: and + description: Whether to join filter values with AND/OR logic. + in: query + name: op + type: string produces: - application/json responses: diff --git a/internal/handler/contact.go b/internal/handler/contact.go index cdced2e..e9f67b7 100644 --- a/internal/handler/contact.go +++ b/internal/handler/contact.go @@ -4,9 +4,11 @@ import ( "encoding/json" "github.com/crazybolillo/eryth/internal/query" "github.com/crazybolillo/eryth/internal/service" + "github.com/crazybolillo/eryth/pkg/model" "github.com/go-chi/chi/v5" "log/slog" "net/http" + "strings" ) type Contact struct { @@ -23,6 +25,9 @@ func (p *Contact) Router() chi.Router { // @Summary List all contacts in the system. // @Param page query int false "Zero based page to fetch" default(0) // @Param pageSize query int false "Max amount of results to be returned" default(20) +// @Param name query string false "Filter by name. Asterisks can be used to represent any character(s)" +// @Param phone query string false "Filter by phone number. Asterisks can be used to represent any character(s)" +// @Param op query string false "Whether to join filter values with AND/OR logic." default(and) // @Produce json // @Success 200 {object} model.ContactPage // @Tags contacts @@ -40,7 +45,16 @@ func (p *Contact) list(w http.ResponseWriter, r *http.Request) { return } - res, err := p.Service.Paginate(r.Context(), page, pageSize) + res, err := p.Service.Paginate( + r.Context(), + model.ContactPageFilter{ + Name: strings.ReplaceAll(r.URL.Query().Get("name"), "*", "%"), + Phone: strings.ReplaceAll(r.URL.Query().Get("phone"), "*", "%"), + Operator: strings.ToLower(r.URL.Query().Get("op")), + }, + page, + pageSize, + ) if err != nil { w.WriteHeader(http.StatusInternalServerError) slog.Error("Failed to list contacts", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) diff --git a/internal/service/contact.go b/internal/service/contact.go index ea09635..bcb183a 100644 --- a/internal/service/contact.go +++ b/internal/service/contact.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "github.com/crazybolillo/eryth/internal/db" "github.com/crazybolillo/eryth/internal/sqlc" "github.com/crazybolillo/eryth/pkg/model" "github.com/jackc/pgx/v5" @@ -12,18 +13,25 @@ type Contact struct { Cursor } -func (c *Contact) Paginate(ctx context.Context, page, size int) (model.ContactPage, error) { +func (c *Contact) Paginate(ctx context.Context, filter model.ContactPageFilter, page, size int) (model.ContactPage, error) { queries := sqlc.New(c.Cursor) rows, err := queries.ListContacts(ctx, sqlc.ListContactsParams{ + Name: db.Text(filter.Name), + Phone: db.Text(filter.Phone), Limit: int32(size), Offset: int32(page), + Op: db.Text(filter.Operator), }) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return model.ContactPage{}, err } - count, err := queries.CountEndpoints(ctx) + count, err := queries.CountContacts(ctx, sqlc.CountContactsParams{ + Name: db.Text(filter.Name), + Phone: db.Text(filter.Phone), + Op: db.Text(filter.Operator), + }) if err != nil { return model.ContactPage{}, err } diff --git a/internal/sqlc/queries.sql.go b/internal/sqlc/queries.sql.go index b930627..2d4dcf1 100644 --- a/internal/sqlc/queries.sql.go +++ b/internal/sqlc/queries.sql.go @@ -18,10 +18,20 @@ FROM ps_endpoints pe INNER JOIN ery_extension ee ON ee.endpoint_id = pe.sid +WHERE CASE + WHEN $1 = 'or' THEN (pe.callerid ILIKE '"' || $2 || '" <%>') OR (ee.extension LIKE $3) + ELSE (pe.callerid ILIKE '"' || $2 || '" <%>' OR $2 IS NULL) AND (ee.extension LIKE $3 OR $3 IS NULL) +END ` -func (q *Queries) CountContacts(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, countContacts) +type CountContactsParams struct { + Op interface{} `json:"op"` + Name pgtype.Text `json:"name"` + Phone pgtype.Text `json:"phone"` +} + +func (q *Queries) CountContacts(ctx context.Context, arg CountContactsParams) (int64, error) { + row := q.db.QueryRow(ctx, countContacts, arg.Op, arg.Name, arg.Phone) var count int64 err := row.Scan(&count) return count, err @@ -157,6 +167,10 @@ FROM ps_endpoints pe INNER JOIN ery_extension ee ON ee.endpoint_id = pe.sid +WHERE CASE + WHEN $3 = 'or' THEN (pe.callerid ILIKE '"' || $4 || '" <%>') OR (ee.extension LIKE $5) + ELSE (pe.callerid ILIKE '"' || $4 || '" <%>' OR $4 IS NULL) AND (ee.extension LIKE $5 OR $5 IS NULL) +END LIMIT $1 OFFSET @@ -164,8 +178,11 @@ OFFSET ` type ListContactsParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` + Op interface{} `json:"op"` + Name pgtype.Text `json:"name"` + Phone pgtype.Text `json:"phone"` } type ListContactsRow struct { @@ -175,7 +192,13 @@ type ListContactsRow struct { } func (q *Queries) ListContacts(ctx context.Context, arg ListContactsParams) ([]ListContactsRow, error) { - rows, err := q.db.Query(ctx, listContacts, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, listContacts, + arg.Limit, + arg.Offset, + arg.Op, + arg.Name, + arg.Phone, + ) if err != nil { return nil, err } diff --git a/pkg/model/contact.go b/pkg/model/contact.go index af4e146..79e503c 100644 --- a/pkg/model/contact.go +++ b/pkg/model/contact.go @@ -11,3 +11,9 @@ type ContactPage struct { Retrieved int `json:"retrieved"` Contacts []Contact `json:"contacts"` } + +type ContactPageFilter struct { + Name string + Phone string + Operator string +} diff --git a/queries.sql b/queries.sql index 3c61f59..deb038a 100644 --- a/queries.sql +++ b/queries.sql @@ -138,6 +138,10 @@ FROM ps_endpoints pe INNER JOIN ery_extension ee ON ee.endpoint_id = pe.sid +WHERE CASE + WHEN @op = 'or' THEN (pe.callerid ILIKE '"' || @name || '" <%>') OR (ee.extension LIKE @phone) + ELSE (pe.callerid ILIKE '"' || @name || '" <%>' OR @name IS NULL) AND (ee.extension LIKE @phone OR @phone IS NULL) +END LIMIT $1 OFFSET @@ -149,4 +153,8 @@ SELECT FROM ps_endpoints pe INNER JOIN - ery_extension ee ON ee.endpoint_id = pe.sid; + ery_extension ee ON ee.endpoint_id = pe.sid +WHERE CASE + WHEN @op = 'or' THEN (pe.callerid ILIKE '"' || @name || '" <%>') OR (ee.extension LIKE @phone) + ELSE (pe.callerid ILIKE '"' || @name || '" <%>' OR @name IS NULL) AND (ee.extension LIKE @phone OR @phone IS NULL) +END;