Skip to content

Commit

Permalink
feat: add cdr endpoint
Browse files Browse the repository at this point in the history
This endpoint makes it possible to paginate through call detail records.
  • Loading branch information
crazybolillo committed Nov 9, 2024
1 parent 65d84fc commit ef7f573
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func serve(ctx context.Context) error {
phonebook := handler.Contact{Service: &service.Contact{Cursor: pool}}
r.Mount("/contacts", phonebook.Router())

cdr := handler.Cdr{Service: &service.Cdr{Cursor: pool}}
r.Mount("/cdr", cdr.Router())

r.Mount("/metrics", promhttp.Handler())

listen := os.Getenv("LISTEN_ADDR")
Expand Down
55 changes: 55 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ definitions:
destination:
type: string
type: object
model.CallRecord:
properties:
answer:
type: string
billsec:
type: integer
context:
type: string
duration:
type: integer
end:
type: string
from:
type: string
id:
type: integer
start:
type: string
to:
type: string
type: object
model.CallRecordPage:
properties:
records:
items:
$ref: '#/definitions/model.CallRecord'
type: array
retrieved:
type: integer
total:
type: integer
type: object
model.Contact:
properties:
id:
Expand Down Expand Up @@ -169,6 +201,29 @@ paths:
provide details on how
tags:
- bouncer
/cdr:
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.CallRecordPage'
summary: List call detail records.
tags:
- cdr
/contacts:
get:
parameters:
Expand Down
62 changes: 62 additions & 0 deletions internal/handler/cdr.go
Original file line number Diff line number Diff line change
@@ -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 Cdr struct {
Service *service.Cdr
}

func (c *Cdr) Router() chi.Router {
r := chi.NewRouter()
r.Get("/", c.list)

return r
}

// @Summary List call detail records.
// @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.CallRecordPage
// @Tags cdr
// @Router /cdr [get]
func (c *Cdr) 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", 25)
if err != nil || page < 0 || pageSize < 0 {
w.WriteHeader(http.StatusBadRequest)
return
}

res, err := c.Service.Paginate(r.Context(), page, pageSize)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to list cdr", "path", r.URL.Path, "reason", err)
return
}

content, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error("Failed to marshall response", "path", r.URL.Path, "reason", err)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(content)
if err != nil {
slog.Error("Failed to write response", "path", r.URL.Path, "reason", err)
}
}
62 changes: 62 additions & 0 deletions internal/service/cdr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package service

import (
"context"
"errors"
"github.com/crazybolillo/eryth/internal/sqlc"
"github.com/crazybolillo/eryth/pkg/model"
"github.com/jackc/pgx/v5"
"time"
)

type Cdr struct {
Cursor Cursor
}

func (c *Cdr) Paginate(ctx context.Context, page, size int) (model.CallRecordPage, error) {
queries := sqlc.New(c.Cursor)

rows, err := queries.ListCallRecords(ctx, sqlc.ListCallRecordsParams{
Limit: int32(size),
Offset: int32(page),
})
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return model.CallRecordPage{}, err
}

count, err := queries.CountCallRecords(ctx)
if err != nil {
return model.CallRecordPage{}, err
}

records := make([]model.CallRecord, len(rows))
for idx, row := range rows {
var answer *time.Time
answerVal, ok := row.Answer.(time.Time)
if !ok {
answer = nil
} else {
answer = &answerVal
}

records[idx] = model.CallRecord{
ID: row.ID,
From: row.Origin.String,
To: row.Destination.String,
Context: row.Context.String,
Start: row.CallStart.Time,
Answer: answer,
End: row.CallEnd.Time,
Duration: row.Duration.Int32,
BillSeconds: row.Billsec.Int32,
}
}

res := model.CallRecordPage{
Records: records,
Total: count,
Retrieved: len(records),
}

return res, nil
}
79 changes: 79 additions & 0 deletions internal/sqlc/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions pkg/model/cdr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package model

import "time"

type CallRecord struct {
ID int64 `json:"id"`
From string `json:"from"`
To string `json:"to"`
Context string `json:"context"`
Start time.Time `json:"start"`
Answer *time.Time `json:"answer"`
End time.Time `json:"end"`
Duration int32 `json:"duration"`
BillSeconds int32 `json:"billsec"`
}

type CallRecordPage struct {
Total int64 `json:"total"`
Retrieved int `json:"retrieved"`
Records []CallRecord `json:"records"`
}
23 changes: 23 additions & 0 deletions queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,26 @@ 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;

-- name: ListCallRecords :many
SELECT
id,
COALESCE(accountcode, src) AS origin,
COALESCE(userfield, dst) AS destination,
dcontext AS context,
cstart AS call_start,
CASE
WHEN answer = '1970-01-01 00:00:00' THEN NULL
ELSE answer
END AS answer,
cend AS call_end,
duration,
billsec
FROM
cdr
ORDER BY
cstart DESC
LIMIT $1 OFFSET $2;

-- name: CountCallRecords :one
SELECT COUNT(*) FROM cdr;

0 comments on commit ef7f573

Please sign in to comment.