Calls statistics
This commit is contained in:
parent
d42abd339d
commit
1e2e606130
9 changed files with 339 additions and 11 deletions
|
@ -35,14 +35,17 @@ type Store interface {
|
|||
|
||||
// Calls gets paginated Calls.
|
||||
Calls(ctx context.Context, p CallsParams) (calls []database.ListCallsPRow, totalCount int, err error)
|
||||
|
||||
// CallStats gets call stats by interval.
|
||||
CallStats(ctx context.Context, interval calls.StatsInterval, start, end jsontypes.Time) (*calls.Stats, error)
|
||||
}
|
||||
|
||||
type store struct {
|
||||
type postgresStore struct {
|
||||
db database.Store
|
||||
}
|
||||
|
||||
func NewStore(db database.Store) *store {
|
||||
return &store{
|
||||
func NewStore(db database.Store) *postgresStore {
|
||||
return &postgresStore{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +89,7 @@ func toAddCallParams(call *calls.Call) database.AddCallParams {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
|
||||
func (s *postgresStore) AddCall(ctx context.Context, call *calls.Call) error {
|
||||
_, err := rbac.Check(ctx, call, rbac.WithActions(entities.ActionCreate))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -124,7 +127,7 @@ func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
|
||||
func (s *postgresStore) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
|
||||
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -145,7 +148,7 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
||||
func (s *postgresStore) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
||||
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -195,7 +198,7 @@ type CallsParams struct {
|
|||
AtLeastSeconds *float32 `json:"atLeastSeconds"`
|
||||
}
|
||||
|
||||
func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) {
|
||||
func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) {
|
||||
_, err = rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
@ -253,7 +256,7 @@ func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListC
|
|||
return rows, int(count), err
|
||||
}
|
||||
|
||||
func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
func (s *postgresStore) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
callOwn, err := s.getCallOwner(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -267,7 +270,7 @@ func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
|
|||
return database.FromCtx(ctx).DeleteCall(ctx, id)
|
||||
}
|
||||
|
||||
func (s *store) getCallOwner(ctx context.Context, id uuid.UUID) (calls.Call, error) {
|
||||
func (s *postgresStore) getCallOwner(ctx context.Context, id uuid.UUID) (calls.Call, error) {
|
||||
subInt, err := database.FromCtx(ctx).GetCallSubmitter(ctx, id)
|
||||
|
||||
var sub *users.UserID
|
||||
|
@ -277,3 +280,35 @@ func (s *store) getCallOwner(ctx context.Context, id uuid.UUID) (calls.Call, err
|
|||
}
|
||||
return calls.Call{ID: id, Submitter: sub}, err
|
||||
}
|
||||
|
||||
func (s *postgresStore) CallStats(ctx context.Context, interval calls.StatsInterval, start, end jsontypes.Time) (*calls.Stats, error) {
|
||||
if !interval.IsValid() {
|
||||
return nil, calls.ErrInvalidInterval
|
||||
}
|
||||
|
||||
cs := &calls.Stats{
|
||||
Interval: interval,
|
||||
}
|
||||
|
||||
_, err := rbac.Check(ctx, cs, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
dbs, err := db.GetCallStatsByInterval(ctx, string(interval), start.PGTypeTSTZ(), end.PGTypeTSTZ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs.Stats = make([]calls.Stat, 0, len(dbs))
|
||||
for _, st := range dbs {
|
||||
cs.Stats = append(cs.Stats, calls.Stat{
|
||||
Count: st.Count,
|
||||
Time: jsontypes.Time(st.Date.Time),
|
||||
})
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
|
46
pkg/calls/stats.go
Normal file
46
pkg/calls/stats.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package calls
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
Stats []Stat `json:"stats"`
|
||||
Interval StatsInterval `json:"interval"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidInterval = errors.New("invalid interval")
|
||||
)
|
||||
|
||||
func (s *Stats) GetResourceName() string {
|
||||
return "CallStats"
|
||||
}
|
||||
|
||||
type StatsInterval string
|
||||
|
||||
const (
|
||||
IntervalMinute StatsInterval = "minute"
|
||||
IntervalHour StatsInterval = "hour"
|
||||
IntervalDay StatsInterval = "day"
|
||||
IntervalWeek StatsInterval = "week"
|
||||
IntervalMonth StatsInterval = "month"
|
||||
IntervalQuarter StatsInterval = "quarter"
|
||||
IntervalYear StatsInterval = "year"
|
||||
)
|
||||
|
||||
func (si StatsInterval) IsValid() bool {
|
||||
switch si {
|
||||
case IntervalMinute, IntervalHour, IntervalDay, IntervalWeek, IntervalMonth, IntervalQuarter, IntervalYear:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type Stat struct {
|
||||
Count int64 `json:"count"`
|
||||
Time jsontypes.Time `json:"time"`
|
||||
}
|
|
@ -1520,6 +1520,128 @@ func (_c *Store_GetCallAudioByID_Call) RunAndReturn(run func(context.Context, uu
|
|||
return _c
|
||||
}
|
||||
|
||||
// GetCallStatsByInterval provides a mock function with given fields: ctx, truncField, start, end
|
||||
func (_m *Store) GetCallStatsByInterval(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]database.GetCallStatsByIntervalRow, error) {
|
||||
ret := _m.Called(ctx, truncField, start, end)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetCallStatsByInterval")
|
||||
}
|
||||
|
||||
var r0 []database.GetCallStatsByIntervalRow
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) ([]database.GetCallStatsByIntervalRow, error)); ok {
|
||||
return rf(ctx, truncField, start, end)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) []database.GetCallStatsByIntervalRow); ok {
|
||||
r0 = rf(ctx, truncField, start, end)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]database.GetCallStatsByIntervalRow)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) error); ok {
|
||||
r1 = rf(ctx, truncField, start, end)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_GetCallStatsByInterval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCallStatsByInterval'
|
||||
type Store_GetCallStatsByInterval_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetCallStatsByInterval is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - truncField string
|
||||
// - start pgtype.Timestamptz
|
||||
// - end pgtype.Timestamptz
|
||||
func (_e *Store_Expecter) GetCallStatsByInterval(ctx interface{}, truncField interface{}, start interface{}, end interface{}) *Store_GetCallStatsByInterval_Call {
|
||||
return &Store_GetCallStatsByInterval_Call{Call: _e.mock.On("GetCallStatsByInterval", ctx, truncField, start, end)}
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByInterval_Call) Run(run func(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz)) *Store_GetCallStatsByInterval_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(pgtype.Timestamptz), args[3].(pgtype.Timestamptz))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByInterval_Call) Return(_a0 []database.GetCallStatsByIntervalRow, _a1 error) *Store_GetCallStatsByInterval_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByInterval_Call) RunAndReturn(run func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) ([]database.GetCallStatsByIntervalRow, error)) *Store_GetCallStatsByInterval_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetCallStatsByTalkgroup provides a mock function with given fields: ctx, truncField, start, end
|
||||
func (_m *Store) GetCallStatsByTalkgroup(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]database.GetCallStatsByTalkgroupRow, error) {
|
||||
ret := _m.Called(ctx, truncField, start, end)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetCallStatsByTalkgroup")
|
||||
}
|
||||
|
||||
var r0 []database.GetCallStatsByTalkgroupRow
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) ([]database.GetCallStatsByTalkgroupRow, error)); ok {
|
||||
return rf(ctx, truncField, start, end)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) []database.GetCallStatsByTalkgroupRow); ok {
|
||||
r0 = rf(ctx, truncField, start, end)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]database.GetCallStatsByTalkgroupRow)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) error); ok {
|
||||
r1 = rf(ctx, truncField, start, end)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_GetCallStatsByTalkgroup_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCallStatsByTalkgroup'
|
||||
type Store_GetCallStatsByTalkgroup_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetCallStatsByTalkgroup is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - truncField string
|
||||
// - start pgtype.Timestamptz
|
||||
// - end pgtype.Timestamptz
|
||||
func (_e *Store_Expecter) GetCallStatsByTalkgroup(ctx interface{}, truncField interface{}, start interface{}, end interface{}) *Store_GetCallStatsByTalkgroup_Call {
|
||||
return &Store_GetCallStatsByTalkgroup_Call{Call: _e.mock.On("GetCallStatsByTalkgroup", ctx, truncField, start, end)}
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByTalkgroup_Call) Run(run func(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz)) *Store_GetCallStatsByTalkgroup_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(pgtype.Timestamptz), args[3].(pgtype.Timestamptz))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByTalkgroup_Call) Return(_a0 []database.GetCallStatsByTalkgroupRow, _a1 error) *Store_GetCallStatsByTalkgroup_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCallStatsByTalkgroup_Call) RunAndReturn(run func(context.Context, string, pgtype.Timestamptz, pgtype.Timestamptz) ([]database.GetCallStatsByTalkgroupRow, error)) *Store_GetCallStatsByTalkgroup_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetCallSubmitter provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
|
|
@ -35,6 +35,8 @@ type Querier interface {
|
|||
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
|
||||
GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error)
|
||||
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
|
||||
GetCallStatsByInterval(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]GetCallStatsByIntervalRow, error)
|
||||
GetCallStatsByTalkgroup(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]GetCallStatsByTalkgroupRow, error)
|
||||
GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error)
|
||||
GetDatabaseSize(ctx context.Context) (string, error)
|
||||
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
|
||||
|
|
97
pkg/database/stats.sql.go
Normal file
97
pkg/database/stats.sql.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: stats.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getCallStatsByInterval = `-- name: GetCallStatsByInterval :many
|
||||
SELECT
|
||||
COUNT(*),
|
||||
date_trunc($1, c.call_date)::TIMESTAMPTZ date
|
||||
FROM calls c
|
||||
WHERE
|
||||
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date >= $2 ELSE TRUE END AND
|
||||
CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date <= $3 ELSE TRUE END
|
||||
GROUP BY 2
|
||||
`
|
||||
|
||||
type GetCallStatsByIntervalRow struct {
|
||||
Count int64 `json:"count"`
|
||||
Date pgtype.Timestamptz `json:"date"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCallStatsByInterval(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]GetCallStatsByIntervalRow, error) {
|
||||
rows, err := q.db.Query(ctx, getCallStatsByInterval, truncField, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCallStatsByIntervalRow
|
||||
for rows.Next() {
|
||||
var i GetCallStatsByIntervalRow
|
||||
if err := rows.Scan(&i.Count, &i.Date); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getCallStatsByTalkgroup = `-- name: GetCallStatsByTalkgroup :many
|
||||
SELECT
|
||||
COUNT(*),
|
||||
c.system,
|
||||
c.talkgroup,
|
||||
date_trunc($1, c.call_date)
|
||||
FROM calls c
|
||||
WHERE
|
||||
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date >= $2 ELSE TRUE END AND
|
||||
CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date <= $3 ELSE TRUE END
|
||||
GROUP BY 2, 3, 4
|
||||
`
|
||||
|
||||
type GetCallStatsByTalkgroupRow struct {
|
||||
Count int64 `json:"count"`
|
||||
System int `json:"system"`
|
||||
Talkgroup int `json:"talkgroup"`
|
||||
DateTrunc pgtype.Interval `json:"dateTrunc"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCallStatsByTalkgroup(ctx context.Context, truncField string, start pgtype.Timestamptz, end pgtype.Timestamptz) ([]GetCallStatsByTalkgroupRow, error) {
|
||||
rows, err := q.db.Query(ctx, getCallStatsByTalkgroup, truncField, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetCallStatsByTalkgroupRow
|
||||
for rows.Next() {
|
||||
var i GetCallStatsByTalkgroupRow
|
||||
if err := rows.Scan(
|
||||
&i.Count,
|
||||
&i.System,
|
||||
&i.Talkgroup,
|
||||
&i.DateTrunc,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
|
@ -22,6 +22,7 @@ const (
|
|||
ResourceAlert = "Alert"
|
||||
ResourceShare = "Share"
|
||||
ResourceAPIKey = "APIKey"
|
||||
ResourceCallStats = "CallStats"
|
||||
|
||||
ActionRead = "read"
|
||||
ActionCreate = "create"
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrBadSubject = errors.New("bad subject in token")
|
||||
ErrBadSubject = errors.New("bad subject in token")
|
||||
ErrAccessDenied = errors.New("access denied")
|
||||
)
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ func (u *User) GetName() string {
|
|||
}
|
||||
|
||||
func (u *User) String() string {
|
||||
return "USER:"+u.GetName()
|
||||
return "USER:" + u.GetName()
|
||||
}
|
||||
|
||||
func (u *User) GetRoles() []string {
|
||||
|
|
25
sql/postgres/queries/stats.sql
Normal file
25
sql/postgres/queries/stats.sql
Normal file
|
@ -0,0 +1,25 @@
|
|||
-- name: GetCallStatsByTalkgroup :many
|
||||
SELECT
|
||||
COUNT(*),
|
||||
c.system,
|
||||
c.talkgroup,
|
||||
date_trunc(@trunc_field, c.call_date)
|
||||
FROM calls c
|
||||
WHERE
|
||||
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date >= @start ELSE TRUE END AND
|
||||
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date <= sqlc.narg('end') ELSE TRUE END
|
||||
GROUP BY 2, 3, 4;
|
||||
|
||||
-- name: GetCallStatsByInterval :many
|
||||
SELECT
|
||||
COUNT(*),
|
||||
date_trunc(@trunc_field, c.call_date)::TIMESTAMPTZ date
|
||||
FROM calls c
|
||||
WHERE
|
||||
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date >= @start ELSE TRUE END AND
|
||||
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
c.call_date <= sqlc.narg('end') ELSE TRUE END
|
||||
GROUP BY 2;
|
Loading…
Add table
Reference in a new issue