diff --git a/cmd/main.go b/cmd/main.go index 141f5d0..4797b15 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -70,10 +70,12 @@ func serve(ctx context.Context) error { endpoint := handler.Endpoint{Service: &service.EndpointService{Cursor: pool}} r.Mount("/endpoints", endpoint.Router()) - checker := &service.Bouncer{Cursor: pool} - authorization := handler.Authorization{Bouncer: checker} + authorization := handler.Authorization{Bouncer: &service.Bouncer{Cursor: pool}} r.Mount("/bouncer", authorization.Router()) + phonebook := handler.Contact{Service: &service.Contact{Cursor: pool}} + r.Mount("/contacts", phonebook.Router()) + listen := os.Getenv("LISTEN_ADDR") if listen == "" { listen = ":8080" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ade1e9c..87645e0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -15,6 +15,26 @@ definitions: destination: type: string type: object + model.Contact: + properties: + id: + type: string + name: + type: string + phone: + type: string + type: object + model.ContactPage: + properties: + contacts: + items: + $ref: '#/definitions/model.Contact' + type: array + retrieved: + type: integer + total: + type: integer + type: object model.Endpoint: properties: accountCode: @@ -149,6 +169,29 @@ paths: provide details on how tags: - bouncer + /contacts: + get: + parameters: + - default: 0 + description: Zero based page to fetch + in: query + name: page + type: integer + - default: 20 + description: Max amount of results to be returned + in: query + name: pageSize + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.ContactPage' + summary: List all contacts in the system. + tags: + - contacts /endpoints: get: parameters: diff --git a/internal/handler/contact.go b/internal/handler/contact.go new file mode 100644 index 0000000..7c610fe --- /dev/null +++ b/internal/handler/contact.go @@ -0,0 +1,62 @@ +package handler + +import ( + "encoding/json" + "github.com/crazybolillo/eryth/internal/query" + "github.com/crazybolillo/eryth/internal/service" + "github.com/go-chi/chi/v5" + "log/slog" + "net/http" +) + +type Contact struct { + Service *service.Contact +} + +func (p *Contact) Router() chi.Router { + r := chi.NewRouter() + r.Get("/", p.list) + + return r +} + +// @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) +// @Produce json +// @Success 200 {object} model.ContactPage +// @Tags contacts +// @Router /contacts [get] +func (p *Contact) list(w http.ResponseWriter, r *http.Request) { + page, err := query.GetIntOr(r.URL.Query(), "page", 0) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + pageSize, err := query.GetIntOr(r.URL.Query(), "pageSize", 10) + if err != nil || page < 0 || pageSize < 0 { + w.WriteHeader(http.StatusBadRequest) + return + } + + res, err := p.Service.Paginate(r.Context(), 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())) + return + } + + content, err := json.Marshal(res) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + slog.Error("Failed to marshall response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(content) + if err != nil { + slog.Error("Failed to write response", slog.String("path", r.URL.Path), slog.String("reason", err.Error())) + } +} diff --git a/internal/model/contact.go b/internal/model/contact.go new file mode 100644 index 0000000..af4e146 --- /dev/null +++ b/internal/model/contact.go @@ -0,0 +1,13 @@ +package model + +type Contact struct { + ID string `json:"id"` + Name string `json:"name"` + Phone string `json:"phone"` +} + +type ContactPage struct { + Total int64 `json:"total"` + Retrieved int `json:"retrieved"` + Contacts []Contact `json:"contacts"` +} diff --git a/internal/service/contact.go b/internal/service/contact.go new file mode 100644 index 0000000..54bd8a8 --- /dev/null +++ b/internal/service/contact.go @@ -0,0 +1,46 @@ +package service + +import ( + "context" + "errors" + "github.com/crazybolillo/eryth/internal/model" + "github.com/crazybolillo/eryth/internal/sqlc" + "github.com/jackc/pgx/v5" +) + +type Contact struct { + Cursor +} + +func (c *Contact) Paginate(ctx context.Context, page, size int) (model.ContactPage, error) { + queries := sqlc.New(c.Cursor) + + rows, err := queries.ListContacts(ctx, sqlc.ListContactsParams{ + Limit: int32(size), + Offset: int32(page), + }) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return model.ContactPage{}, err + } + + count, err := queries.CountEndpoints(ctx) + if err != nil { + return model.ContactPage{}, err + } + + contacts := make([]model.Contact, len(rows)) + for idx, row := range rows { + contacts[idx] = model.Contact{ + ID: row.ID, + Name: displayNameFromClid(row.Callerid.String), + Phone: row.Extension.String, + } + } + res := model.ContactPage{ + Total: count, + Retrieved: len(rows), + Contacts: contacts, + } + + return res, nil +} diff --git a/internal/sqlc/queries.sql.go b/internal/sqlc/queries.sql.go index 8533135..b930627 100644 --- a/internal/sqlc/queries.sql.go +++ b/internal/sqlc/queries.sql.go @@ -11,6 +11,22 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const countContacts = `-- name: CountContacts :one +SELECT + COUNT(*) +FROM + ps_endpoints pe +INNER JOIN + ery_extension ee ON ee.endpoint_id = pe.sid +` + +func (q *Queries) CountContacts(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, countContacts) + var count int64 + err := row.Scan(&count) + return count, err +} + const countEndpoints = `-- name: CountEndpoints :one SELECT COUNT(*) FROM ps_endpoints ` @@ -134,6 +150,50 @@ func (q *Queries) GetEndpointByID(ctx context.Context, sid int32) (GetEndpointBy return i, err } +const listContacts = `-- name: ListContacts :many +SELECT + pe.id, pe.callerid, ee.extension +FROM + ps_endpoints pe +INNER JOIN + ery_extension ee ON ee.endpoint_id = pe.sid +LIMIT + $1 +OFFSET + $2 +` + +type ListContactsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type ListContactsRow struct { + ID string `json:"id"` + Callerid pgtype.Text `json:"callerid"` + Extension pgtype.Text `json:"extension"` +} + +func (q *Queries) ListContacts(ctx context.Context, arg ListContactsParams) ([]ListContactsRow, error) { + rows, err := q.db.Query(ctx, listContacts, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListContactsRow + for rows.Next() { + var i ListContactsRow + if err := rows.Scan(&i.ID, &i.Callerid, &i.Extension); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listEndpoints = `-- name: ListEndpoints :many SELECT pe.sid, pe.id, pe.callerid, pe.context, ee.extension diff --git a/queries.sql b/queries.sql index 53d0e40..3c61f59 100644 --- a/queries.sql +++ b/queries.sql @@ -131,3 +131,22 @@ SET WHERE id = $2; +-- name: ListContacts :many +SELECT + pe.id, pe.callerid, ee.extension +FROM + ps_endpoints pe +INNER JOIN + ery_extension ee ON ee.endpoint_id = pe.sid +LIMIT + $1 +OFFSET + $2; + +-- name: CountContacts :one +SELECT + COUNT(*) +FROM + ps_endpoints pe +INNER JOIN + ery_extension ee ON ee.endpoint_id = pe.sid;