Merge pull request 'Pagination' (#49) from paginateTG into trunk

Reviewed-on: #49
This commit is contained in:
Daniel 2024-11-24 00:14:40 -05:00
commit 80c06919f8
15 changed files with 491 additions and 37 deletions

View file

@ -36,7 +36,7 @@ func PtrTo[T any](t T) *T {
return &t return &t
} }
func PtrOrNull[T comparable](val T) *T { func NilIfZero[T comparable](val T) *T {
var zero T var zero T
if val == zero { if val == zero {
return nil return nil
@ -45,7 +45,7 @@ func PtrOrNull[T comparable](val T) *T {
return &val return &val
} }
func ZeroOr[T any](v *T) T { func ZeroIfNil[T any](v *T) T {
var zero T var zero T
if v == nil { if v == nil {
return zero return zero
@ -53,3 +53,16 @@ func ZeroOr[T any](v *T) T {
return *v return *v
} }
func DefaultIfNilOrZero[T comparable](v *T, def T) T {
if v == nil {
return def
}
var zero T
if *v == zero {
return def
}
return *v
}

8
internal/forms/testdata/urlenc3.http vendored Normal file
View file

@ -0,0 +1,8 @@
POST /api/talkgroup/ HTTP/1.1
Host: xenon:3051
User-Agent: curl/8.10.1
Accept: */*
Content-Length: 16
Content-Type: application/x-www-form-urlencoded
page=1&perPage=2&orderBy=id

View file

@ -223,7 +223,15 @@ func (o *options) unmIterFields(r *http.Request, destStruct reflect.Value) error
} }
destFieldVal.Set(reflect.ValueOf(ar)) destFieldVal.Set(reflect.ValueOf(ar))
default: default:
panic(fmt.Errorf("unsupported type %T", v)) dvt := destFieldVal.Type()
if dvt.Kind() == reflect.Ptr {
dvt = dvt.Elem()
}
if reflect.ValueOf(ff).CanConvert(dvt) {
setVal(destFieldVal, ff != "" || o.acceptBlank, ff)
} else {
panic(fmt.Errorf("unsupported type %T", v))
}
} }
} }

View file

@ -14,6 +14,7 @@ import (
"dynatron.me/x/stillbox/pkg/alerting" "dynatron.me/x/stillbox/pkg/alerting"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -115,6 +116,14 @@ var (
TalkgroupGroup: "Wide Area", TalkgroupGroup: "Wide Area",
TalkgroupLabel: "Wide Area 1 FD/EMS Intercity", TalkgroupLabel: "Wide Area 1 FD/EMS Intercity",
} }
Pag1 = tgstore.Pagination{
Pagination: common.Pagination{
Page: common.PtrTo(1),
PerPage: common.PtrTo(2),
},
OrderBy: common.PtrTo(tgstore.TGOrderID),
}
) )
func makeRequest(fixture string) *http.Request { func makeRequest(fixture string) *http.Request {
@ -222,6 +231,13 @@ func TestUnmarshal(t *testing.T) {
expect: realSim, expect: realSim,
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime()}, opts: []forms.Option{forms.WithAcceptBlank(), forms.WithParseLocalTime()},
}, },
{
name: "urlencode pagination",
r: makeRequest("urlenc3.http"),
dest: &tgstore.Pagination{},
expect: &Pag1,
opts: []forms.Option{forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()},
},
} }
for _, tc := range tests { for _, tc := range tests {

View file

@ -13,12 +13,19 @@ func (g GetTalkgroupWithLearnedRow) GetLearned() bool { return g
func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Talkgroup.Learned } func (g GetTalkgroupsWithLearnedRow) GetLearned() bool { return g.Talkgroup.Learned }
func (g GetTalkgroupsWithLearnedPRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedPRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedPRow) GetLearned() bool { return g.Talkgroup.Learned }
func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup { return g.Talkgroup } func (g GetTalkgroupsWithLearnedBySystemRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System { return g.System } func (g GetTalkgroupsWithLearnedBySystemRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool { return g.Talkgroup.Learned } func (g GetTalkgroupsWithLearnedBySystemRow) GetLearned() bool { return g.Talkgroup.Learned }
func (g Talkgroup) GetTalkgroup() Talkgroup { return g }
func (g Talkgroup) GetSystem() System { return System{ID: int(g.SystemID)} } func (g GetTalkgroupsWithLearnedBySystemPRow) GetTalkgroup() Talkgroup { return g.Talkgroup }
func (g Talkgroup) GetLearned() bool { return false } func (g GetTalkgroupsWithLearnedBySystemPRow) GetSystem() System { return g.System }
func (g GetTalkgroupsWithLearnedBySystemPRow) GetLearned() bool { return g.Talkgroup.Learned }
func (g Talkgroup) GetTalkgroup() Talkgroup { return g }
func (g Talkgroup) GetSystem() System { return System{ID: int(g.SystemID)} }
func (g Talkgroup) GetLearned() bool { return false }
func (g Talkgroup) String() string { func (g Talkgroup) String() string {
return g.StringTag(true) return g.StringTag(true)

View file

@ -1244,6 +1244,127 @@ func (_c *Store_GetTalkgroupsWithLearnedBySystem_Call) RunAndReturn(run func(con
return _c return _c
} }
// GetTalkgroupsWithLearnedBySystemP provides a mock function with given fields: ctx, system, offset, perPage
func (_m *Store) GetTalkgroupsWithLearnedBySystemP(ctx context.Context, system int32, offset int32, perPage int32) ([]database.GetTalkgroupsWithLearnedBySystemPRow, error) {
ret := _m.Called(ctx, system, offset, perPage)
if len(ret) == 0 {
panic("no return value specified for GetTalkgroupsWithLearnedBySystemP")
}
var r0 []database.GetTalkgroupsWithLearnedBySystemPRow
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int32, int32, int32) ([]database.GetTalkgroupsWithLearnedBySystemPRow, error)); ok {
return rf(ctx, system, offset, perPage)
}
if rf, ok := ret.Get(0).(func(context.Context, int32, int32, int32) []database.GetTalkgroupsWithLearnedBySystemPRow); ok {
r0 = rf(ctx, system, offset, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.GetTalkgroupsWithLearnedBySystemPRow)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int32, int32, int32) error); ok {
r1 = rf(ctx, system, offset, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetTalkgroupsWithLearnedBySystemP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithLearnedBySystemP'
type Store_GetTalkgroupsWithLearnedBySystemP_Call struct {
*mock.Call
}
// GetTalkgroupsWithLearnedBySystemP is a helper method to define mock.On call
// - ctx context.Context
// - system int32
// - offset int32
// - perPage int32
func (_e *Store_Expecter) GetTalkgroupsWithLearnedBySystemP(ctx interface{}, system interface{}, offset interface{}, perPage interface{}) *Store_GetTalkgroupsWithLearnedBySystemP_Call {
return &Store_GetTalkgroupsWithLearnedBySystemP_Call{Call: _e.mock.On("GetTalkgroupsWithLearnedBySystemP", ctx, system, offset, perPage)}
}
func (_c *Store_GetTalkgroupsWithLearnedBySystemP_Call) Run(run func(ctx context.Context, system int32, offset int32, perPage int32)) *Store_GetTalkgroupsWithLearnedBySystemP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int32), args[2].(int32), args[3].(int32))
})
return _c
}
func (_c *Store_GetTalkgroupsWithLearnedBySystemP_Call) Return(_a0 []database.GetTalkgroupsWithLearnedBySystemPRow, _a1 error) *Store_GetTalkgroupsWithLearnedBySystemP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetTalkgroupsWithLearnedBySystemP_Call) RunAndReturn(run func(context.Context, int32, int32, int32) ([]database.GetTalkgroupsWithLearnedBySystemPRow, error)) *Store_GetTalkgroupsWithLearnedBySystemP_Call {
_c.Call.Return(run)
return _c
}
// GetTalkgroupsWithLearnedP provides a mock function with given fields: ctx, offset, perPage
func (_m *Store) GetTalkgroupsWithLearnedP(ctx context.Context, offset int32, perPage int32) ([]database.GetTalkgroupsWithLearnedPRow, error) {
ret := _m.Called(ctx, offset, perPage)
if len(ret) == 0 {
panic("no return value specified for GetTalkgroupsWithLearnedP")
}
var r0 []database.GetTalkgroupsWithLearnedPRow
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int32, int32) ([]database.GetTalkgroupsWithLearnedPRow, error)); ok {
return rf(ctx, offset, perPage)
}
if rf, ok := ret.Get(0).(func(context.Context, int32, int32) []database.GetTalkgroupsWithLearnedPRow); ok {
r0 = rf(ctx, offset, perPage)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.GetTalkgroupsWithLearnedPRow)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int32, int32) error); ok {
r1 = rf(ctx, offset, perPage)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetTalkgroupsWithLearnedP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTalkgroupsWithLearnedP'
type Store_GetTalkgroupsWithLearnedP_Call struct {
*mock.Call
}
// GetTalkgroupsWithLearnedP is a helper method to define mock.On call
// - ctx context.Context
// - offset int32
// - perPage int32
func (_e *Store_Expecter) GetTalkgroupsWithLearnedP(ctx interface{}, offset interface{}, perPage interface{}) *Store_GetTalkgroupsWithLearnedP_Call {
return &Store_GetTalkgroupsWithLearnedP_Call{Call: _e.mock.On("GetTalkgroupsWithLearnedP", ctx, offset, perPage)}
}
func (_c *Store_GetTalkgroupsWithLearnedP_Call) Run(run func(ctx context.Context, offset int32, perPage int32)) *Store_GetTalkgroupsWithLearnedP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(int32), args[2].(int32))
})
return _c
}
func (_c *Store_GetTalkgroupsWithLearnedP_Call) Return(_a0 []database.GetTalkgroupsWithLearnedPRow, _a1 error) *Store_GetTalkgroupsWithLearnedP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetTalkgroupsWithLearnedP_Call) RunAndReturn(run func(context.Context, int32, int32) ([]database.GetTalkgroupsWithLearnedPRow, error)) *Store_GetTalkgroupsWithLearnedP_Call {
_c.Call.Return(run)
return _c
}
// GetUserByID provides a mock function with given fields: ctx, id // GetUserByID provides a mock function with given fields: ctx, id
func (_m *Store) GetUserByID(ctx context.Context, id int) (database.User, error) { func (_m *Store) GetUserByID(ctx context.Context, id int) (database.User, error) {
ret := _m.Called(ctx, id) ret := _m.Called(ctx, id)

View file

@ -30,6 +30,8 @@ type Querier interface {
GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error)
GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, error)
GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]GetTalkgroupsWithLearnedBySystemRow, error) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system int32) ([]GetTalkgroupsWithLearnedBySystemRow, error)
GetTalkgroupsWithLearnedBySystemP(ctx context.Context, system int32, offset int32, perPage int32) ([]GetTalkgroupsWithLearnedBySystemPRow, error)
GetTalkgroupsWithLearnedP(ctx context.Context, offset int32, perPage int32) ([]GetTalkgroupsWithLearnedPRow, error)
GetUserByID(ctx context.Context, id int) (User, error) GetUserByID(ctx context.Context, id int) (User, error)
GetUserByUID(ctx context.Context, id int) (User, error) GetUserByUID(ctx context.Context, id int) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)

View file

@ -377,6 +377,112 @@ func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system i
return items, nil return items, nil
} }
const getTalkgroupsWithLearnedBySystemP = `-- name: GetTalkgroupsWithLearnedBySystemP :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, tg.learned, tg.ignored, sys.id, sys.name
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = $1
ORDER BY tg.system_id ASC, tg.tgid ASC
OFFSET $2 ROWS
FETCH NEXT $3 ROWS ONLY
`
type GetTalkgroupsWithLearnedBySystemPRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
}
func (q *Queries) GetTalkgroupsWithLearnedBySystemP(ctx context.Context, system int32, offset int32, perPage int32) ([]GetTalkgroupsWithLearnedBySystemPRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedBySystemP, system, offset, perPage)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupsWithLearnedBySystemPRow
for rows.Next() {
var i GetTalkgroupsWithLearnedBySystemPRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.TGID,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TGGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.Talkgroup.Learned,
&i.Talkgroup.Ignored,
&i.System.ID,
&i.System.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTalkgroupsWithLearnedP = `-- name: GetTalkgroupsWithLearnedP :many
SELECT
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, tg.learned, tg.ignored, sys.id, sys.name
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE ignored IS NOT TRUE
ORDER BY tg.system_id ASC, tg.tgid ASC
OFFSET $1 ROWS
FETCH NEXT $2 ROWS ONLY
`
type GetTalkgroupsWithLearnedPRow struct {
Talkgroup Talkgroup `json:"talkgroup"`
System System `json:"system"`
}
func (q *Queries) GetTalkgroupsWithLearnedP(ctx context.Context, offset int32, perPage int32) ([]GetTalkgroupsWithLearnedPRow, error) {
rows, err := q.db.Query(ctx, getTalkgroupsWithLearnedP, offset, perPage)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTalkgroupsWithLearnedPRow
for rows.Next() {
var i GetTalkgroupsWithLearnedPRow
if err := rows.Scan(
&i.Talkgroup.ID,
&i.Talkgroup.SystemID,
&i.Talkgroup.TGID,
&i.Talkgroup.Name,
&i.Talkgroup.AlphaTag,
&i.Talkgroup.TGGroup,
&i.Talkgroup.Frequency,
&i.Talkgroup.Metadata,
&i.Talkgroup.Tags,
&i.Talkgroup.Alert,
&i.Talkgroup.AlertConfig,
&i.Talkgroup.Weight,
&i.Talkgroup.Learned,
&i.Talkgroup.Ignored,
&i.System.ID,
&i.System.Name,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const restoreTalkgroupVersion = `-- name: RestoreTalkgroupVersion :one const restoreTalkgroupVersion = `-- name: RestoreTalkgroupVersion :one
INSERT INTO talkgroups( INSERT INTO talkgroups(
system_id, system_id,

View file

@ -59,6 +59,14 @@ func badRequest(err error) render.Renderer {
} }
} }
func badRequestErrText(err error) render.Renderer {
return &errResponse{
Err: err,
Code: http.StatusBadRequest,
Error: "Bad request: " + err.Error(),
}
}
func recordNotFound(err error) render.Renderer { func recordNotFound(err error) render.Renderer {
return &errResponse{ return &errResponse{
Err: err, Err: err,
@ -67,7 +75,7 @@ func recordNotFound(err error) render.Renderer {
} }
} }
func errTextNotFound(err error) render.Renderer { func notFoundErrText(err error) render.Renderer {
return &errResponse{ return &errResponse{
Err: err, Err: err,
Code: http.StatusNotFound, Code: http.StatusNotFound,
@ -86,9 +94,10 @@ func internalError(err error) render.Renderer {
type errResponder func(error) render.Renderer type errResponder func(error) render.Renderer
var statusMapping = map[error]errResponder{ var statusMapping = map[error]errResponder{
tgstore.ErrNoSuchSystem: errTextNotFound, tgstore.ErrNoSuchSystem: notFoundErrText,
tgstore.ErrNotFound: errTextNotFound, tgstore.ErrNotFound: notFoundErrText,
pgx.ErrNoRows: recordNotFound, tgstore.ErrInvalidOrderBy: badRequestErrText,
pgx.ErrNoRows: recordNotFound,
} }
func autoError(err error) render.Renderer { func autoError(err error) render.Renderer {

View file

@ -12,6 +12,8 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
const DefaultPerPage = 20
type talkgroupAPI struct { type talkgroupAPI struct {
} }
@ -19,10 +21,15 @@ func (tga *talkgroupAPI) Subrouter() http.Handler {
r := chi.NewMux() r := chi.NewMux()
r.Get(`/{system:\d+}/{id:\d+}`, tga.get) r.Get(`/{system:\d+}/{id:\d+}`, tga.get)
r.Put(`/{system:\d+}/{id:\d+}`, tga.put)
r.Put(`/{system:\d+}`, tga.putTalkgroups)
r.Get(`/{system:\d+}/`, tga.get) r.Get(`/{system:\d+}/`, tga.get)
r.Get("/", tga.get) r.Get("/", tga.get)
r.Put(`/{system:\d+}/{id:\d+}`, tga.put)
r.Put(`/{system:\d+}`, tga.putTalkgroups)
r.Post(`/{system:\d+}/`, tga.postPaginated)
r.Post(`/`, tga.postPaginated)
r.Post("/import", tga.tgImport) r.Post("/import", tga.tgImport)
return r return r
@ -83,6 +90,42 @@ func (tga *talkgroupAPI) get(w http.ResponseWriter, r *http.Request) {
respond(w, r, res) respond(w, r, res)
} }
func (tga *talkgroupAPI) postPaginated(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tgs := tgstore.FromCtx(ctx)
var p tgParams
err := decodeParams(&p, r)
if err != nil {
wErr(w, r, badRequest(err))
return
}
input := &tgstore.Pagination{}
err = forms.Unmarshal(r, input, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
if err != nil {
wErr(w, r, badRequest(err))
return
}
var res interface{}
switch {
case p.System != nil:
res, err = tgs.SystemTGs(ctx, int32(*p.System), tgstore.WithPagination(input, DefaultPerPage))
default:
// get all talkgroups
res, err = tgs.TGs(ctx, nil, tgstore.WithPagination(input, DefaultPerPage))
}
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, res)
}
func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) { func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) {
var id tgParams var id tgParams
err := decodeParams(&id, r) err := decodeParams(&id, r)

View file

@ -73,9 +73,9 @@ func (s *DatabaseSink) toAddCallParams(call *calls.Call) database.AddCallParams
System: call.System, System: call.System,
Talkgroup: call.Talkgroup, Talkgroup: call.Talkgroup,
CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true}, CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true},
AudioName: common.PtrOrNull(call.AudioName), AudioName: common.NilIfZero(call.AudioName),
AudioBlob: call.Audio, AudioBlob: call.Audio,
AudioType: common.PtrOrNull(call.AudioType), AudioType: common.NilIfZero(call.AudioType),
Duration: call.Duration.MsInt32Ptr(), Duration: call.Duration.MsInt32Ptr(),
Frequency: call.Frequency, Frequency: call.Frequency,
Frequencies: call.Frequencies, Frequencies: call.Frequencies,

View file

@ -83,9 +83,9 @@ func (car *CallUploadRequest) ToCall(submitter auth.UserID) (*calls.Call, error)
Frequency: car.Frequency, Frequency: car.Frequency,
Frequencies: car.Frequencies, Frequencies: car.Frequencies,
Patches: car.Patches, Patches: car.Patches,
TalkgroupLabel: common.PtrOrNull(car.TalkgroupLabel), TalkgroupLabel: common.NilIfZero(car.TalkgroupLabel),
TGAlphaTag: common.PtrOrNull(car.TalkgroupTag), TGAlphaTag: common.NilIfZero(car.TalkgroupTag),
TalkgroupGroup: common.PtrOrNull(car.TalkgroupGroup), TalkgroupGroup: common.NilIfZero(car.TalkgroupGroup),
Source: car.Source, Source: car.Source,
}, !car.DontStore) }, !car.DontStore)
} }

View file

@ -21,8 +21,9 @@ import (
type tgMap map[tgsp.ID]*tgsp.Talkgroup type tgMap map[tgsp.ID]*tgsp.Talkgroup
var ( var (
ErrNotFound = errors.New("talkgroup not found") ErrNotFound = errors.New("talkgroup not found")
ErrNoSuchSystem = errors.New("no such system") ErrNoSuchSystem = errors.New("no such system")
ErrInvalidOrderBy = errors.New("invalid pagination orderBy value")
) )
type Store interface { type Store interface {
@ -36,13 +37,13 @@ type Store interface {
TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error)
// TGs retrieves many talkgroups from the Store. // TGs retrieves many talkgroups from the Store.
TGs(ctx context.Context, tgs tgsp.IDs) ([]*tgsp.Talkgroup, error) TGs(ctx context.Context, tgs tgsp.IDs, opts ...option) ([]*tgsp.Talkgroup, error)
// LearnTG learns the talkgroup from a Call. // LearnTG learns the talkgroup from a Call.
LearnTG(ctx context.Context, call *calls.Call) (*tgsp.Talkgroup, error) LearnTG(ctx context.Context, call *calls.Call) (*tgsp.Talkgroup, error)
// SystemTGs retrieves all Talkgroups associated with a System. // SystemTGs retrieves all Talkgroups associated with a System.
SystemTGs(ctx context.Context, systemID int32) ([]*tgsp.Talkgroup, error) SystemTGs(ctx context.Context, systemID int32, opts ...option) ([]*tgsp.Talkgroup, error)
// SystemName retrieves a system name from the store. It returns the record and whether one was found. // SystemName retrieves a system name from the store. It returns the record and whether one was found.
SystemName(ctx context.Context, id int) (string, bool) SystemName(ctx context.Context, id int) (string, bool)
@ -63,6 +64,55 @@ type Store interface {
HUP(*config.Config) HUP(*config.Config)
} }
type options struct {
pagination *Pagination
perPageDefault int
}
func sOpt(opts []option) (o options) {
for _, opt := range opts {
opt(&o)
}
return
}
type option func(*options)
func WithPagination(p *Pagination, defPerPage int) option {
return func(o *options) {
o.pagination = p
o.perPageDefault = defPerPage
}
}
type TGOrder string
const (
TGOrderTGID TGOrder = "tgid"
TGOrderGroup TGOrder = "group"
TGOrderName TGOrder = "name"
TGOrderID TGOrder = "id"
)
func (t *TGOrder) IsValid() bool {
if t == nil {
return true
}
switch *t {
case TGOrderTGID, TGOrderGroup, TGOrderName, TGOrderID:
return true
}
return false
}
type Pagination struct {
common.Pagination
OrderBy *TGOrder `json:"orderBy"`
}
type storeCtxKey string type storeCtxKey string
const StoreCtxKey storeCtxKey = "store" const StoreCtxKey storeCtxKey = "store"
@ -148,20 +198,29 @@ func (t *cache) add(rec *tgsp.Talkgroup) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
t.addNoLock(rec)
}
func (t *cache) addNoLock(rec *tgsp.Talkgroup) {
tg := tgsp.TG(rec.System.ID, rec.Talkgroup.TGID) tg := tgsp.TG(rec.System.ID, rec.Talkgroup.TGID)
t.tgs[tg] = rec t.tgs[tg] = rec
t.systems[int32(rec.System.ID)] = rec.System.Name t.systems[int32(rec.System.ID)] = rec.System.Name
} }
type row interface { type rowType interface {
database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow | database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow |
database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow |
database.GetTalkgroupsWithLearnedBySystemPRow | database.GetTalkgroupsWithLearnedPRow
row
}
type row interface {
GetTalkgroup() database.Talkgroup GetTalkgroup() database.Talkgroup
GetSystem() database.System GetSystem() database.System
GetLearned() bool GetLearned() bool
} }
func rowToTalkgroup[T row](r T) *tgsp.Talkgroup { func rowToTalkgroup[T rowType](r T) *tgsp.Talkgroup {
return &tgsp.Talkgroup{ return &tgsp.Talkgroup{
Talkgroup: r.GetTalkgroup(), Talkgroup: r.GetTalkgroup(),
System: r.GetSystem(), System: r.GetSystem(),
@ -169,10 +228,13 @@ func rowToTalkgroup[T row](r T) *tgsp.Talkgroup {
} }
} }
func addToRowList[T row](t *cache, r []*tgsp.Talkgroup, tgRecords []T) []*tgsp.Talkgroup { func addToRowListS[T rowType](t *cache, r []*tgsp.Talkgroup, tgRecords []T) []*tgsp.Talkgroup {
t.Lock()
defer t.Unlock()
for _, rec := range tgRecords { for _, rec := range tgRecords {
tg := rowToTalkgroup(rec) tg := rowToTalkgroup(rec)
t.add(tg) t.addNoLock(tg)
r = append(r, tg) r = append(r, tg)
} }
@ -180,8 +242,25 @@ func addToRowList[T row](t *cache, r []*tgsp.Talkgroup, tgRecords []T) []*tgsp.T
return r return r
} }
func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs) ([]*tgsp.Talkgroup, error) { func addToRowList[T rowType](t *cache, tgRecords []T) []*tgsp.Talkgroup {
t.Lock()
defer t.Unlock()
r := make([]*tgsp.Talkgroup, 0, len(tgRecords))
for _, rec := range tgRecords {
tg := rowToTalkgroup(rec)
t.addNoLock(tg)
r = append(r, tg)
}
return r
}
func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...option) ([]*tgsp.Talkgroup, error) {
db := database.FromCtx(ctx)
r := make([]*tgsp.Talkgroup, 0, len(tgs)) r := make([]*tgsp.Talkgroup, 0, len(tgs))
opt := sOpt(opts)
var err error var err error
if tgs != nil { if tgs != nil {
toGet := make(tgsp.IDs, 0, len(tgs)) toGet := make(tgsp.IDs, 0, len(tgs))
@ -194,20 +273,30 @@ func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs) ([]*tgsp.Talkgroup, error
} }
} }
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples()) tgRecords, err := db.GetTalkgroupsWithLearnedBySysTGID(ctx, toGet.Tuples())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return addToRowList(t, r, tgRecords), nil return addToRowList(t, tgRecords), nil
} }
// get all talkgroups // get all talkgroups
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearned(ctx) if opt.pagination != nil {
offset, perPage := opt.pagination.OffsetPerPage(opt.perPageDefault)
tgRecords, err := db.GetTalkgroupsWithLearnedP(ctx, offset, perPage)
if err != nil {
return nil, err
}
return addToRowListS(t, r, tgRecords), nil
}
tgRecords, err := db.GetTalkgroupsWithLearned(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return addToRowList(t, r, tgRecords), nil return addToRowListS(t, r, tgRecords), nil
} }
func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error { func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error {
@ -236,14 +325,25 @@ func (t *cache) Weight(ctx context.Context, id tgsp.ID, tm time.Time) float64 {
return float64(m) return float64(m)
} }
func (t *cache) SystemTGs(ctx context.Context, systemID int32) ([]*tgsp.Talkgroup, error) { func (t *cache) SystemTGs(ctx context.Context, systemID int32, opts ...option) ([]*tgsp.Talkgroup, error) {
recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySystem(ctx, systemID) db := database.FromCtx(ctx)
opt := sOpt(opts)
var err error
if opt.pagination != nil {
offset, perPage := opt.pagination.OffsetPerPage(opt.perPageDefault)
recs, err := db.GetTalkgroupsWithLearnedBySystemP(ctx, systemID, offset, perPage)
if err != nil {
return nil, err
}
return addToRowList(t, recs), nil
}
recs, err := db.GetTalkgroupsWithLearnedBySystem(ctx, systemID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := make([]*tgsp.Talkgroup, 0, len(recs)) return addToRowList(t, recs), nil
return addToRowList(t, r, recs), nil
} }
func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) { func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) {

View file

@ -30,8 +30,8 @@ type Alias struct {
func tgToAlias(tg *talkgroups.Talkgroup) Alias { func tgToAlias(tg *talkgroups.Talkgroup) Alias {
return Alias{ return Alias{
XMLName: xml.Name{Local: "alias"}, XMLName: xml.Name{Local: "alias"},
Name: common.ZeroOr(tg.Name), Name: common.ZeroIfNil(tg.Name),
Group: common.ZeroOr(tg.TGGroup), Group: common.ZeroIfNil(tg.TGGroup),
List: "Stillbox", List: "Stillbox",
IDs: []ID{ IDs: []ID{
ID{ ID{

View file

@ -31,6 +31,17 @@ FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
WHERE (tg.system_id, tg.tgid) = (@system_id, @tgid); WHERE (tg.system_id, tg.tgid) = (@system_id, @tgid);
-- name: GetTalkgroupsWithLearnedBySystemP :many
SELECT
sqlc.embed(tg), sqlc.embed(sys)
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE tg.system_id = @system
ORDER BY tg.system_id ASC, tg.tgid ASC
OFFSET sqlc.arg('offset') ROWS
FETCH NEXT sqlc.arg('per_page') ROWS ONLY;
;
-- name: GetTalkgroupsWithLearnedBySystem :many -- name: GetTalkgroupsWithLearnedBySystem :many
SELECT SELECT
sqlc.embed(tg), sqlc.embed(sys) sqlc.embed(tg), sqlc.embed(sys)
@ -45,6 +56,16 @@ FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id JOIN systems sys ON tg.system_id = sys.id
WHERE ignored IS NOT TRUE; WHERE ignored IS NOT TRUE;
-- name: GetTalkgroupsWithLearnedP :many
SELECT
sqlc.embed(tg), sqlc.embed(sys)
FROM talkgroups tg
JOIN systems sys ON tg.system_id = sys.id
WHERE ignored IS NOT TRUE
ORDER BY tg.system_id ASC, tg.tgid ASC
OFFSET sqlc.arg('offset') ROWS
FETCH NEXT sqlc.arg('per_page') ROWS ONLY;
-- name: GetSystemName :one -- name: GetSystemName :one
SELECT name FROM systems WHERE id = @system_id; SELECT name FROM systems WHERE id = @system_id;