diff --git a/internal/common/common.go b/internal/common/common.go index a558427..7e5f8cb 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -36,7 +36,7 @@ func PtrTo[T any](t T) *T { return &t } -func PtrOrNull[T comparable](val T) *T { +func NilIfZero[T comparable](val T) *T { var zero T if val == zero { return nil @@ -45,7 +45,7 @@ func PtrOrNull[T comparable](val T) *T { return &val } -func ZeroOr[T any](v *T) T { +func ZeroIfNil[T any](v *T) T { var zero T if v == nil { return zero @@ -53,3 +53,16 @@ func ZeroOr[T any](v *T) T { 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 +} diff --git a/internal/forms/testdata/urlenc3.http b/internal/forms/testdata/urlenc3.http new file mode 100644 index 0000000..854231b --- /dev/null +++ b/internal/forms/testdata/urlenc3.http @@ -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 diff --git a/internal/forms/unmarshal.go b/internal/forms/unmarshal.go index 4b8f568..2a68789 100644 --- a/internal/forms/unmarshal.go +++ b/internal/forms/unmarshal.go @@ -223,7 +223,15 @@ func (o *options) unmIterFields(r *http.Request, destStruct reflect.Value) error } destFieldVal.Set(reflect.ValueOf(ar)) 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)) + } } } diff --git a/internal/forms/unmarshal_test.go b/internal/forms/unmarshal_test.go index 0051ee1..be0b6bc 100644 --- a/internal/forms/unmarshal_test.go +++ b/internal/forms/unmarshal_test.go @@ -14,6 +14,7 @@ import ( "dynatron.me/x/stillbox/pkg/alerting" "dynatron.me/x/stillbox/pkg/config" + "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -115,6 +116,14 @@ var ( TalkgroupGroup: "Wide Area", 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 { @@ -222,6 +231,13 @@ func TestUnmarshal(t *testing.T) { expect: realSim, 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 { diff --git a/pkg/database/extend.go b/pkg/database/extend.go index 4444ea9..bb4f50c 100644 --- a/pkg/database/extend.go +++ b/pkg/database/extend.go @@ -13,12 +13,19 @@ func (g GetTalkgroupWithLearnedRow) GetLearned() bool { return g func (g GetTalkgroupsWithLearnedRow) GetTalkgroup() Talkgroup { return g.Talkgroup } func (g GetTalkgroupsWithLearnedRow) GetSystem() System { return g.System } 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) GetSystem() System { return g.System } 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 Talkgroup) GetLearned() bool { return false } + +func (g GetTalkgroupsWithLearnedBySystemPRow) GetTalkgroup() Talkgroup { return g.Talkgroup } +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 { return g.StringTag(true) diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index 6d78e98..291e475 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -1244,6 +1244,127 @@ func (_c *Store_GetTalkgroupsWithLearnedBySystem_Call) RunAndReturn(run func(con 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 func (_m *Store) GetUserByID(ctx context.Context, id int) (database.User, error) { ret := _m.Called(ctx, id) diff --git a/pkg/database/querier.go b/pkg/database/querier.go index c9c531b..b9ede53 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -30,6 +30,8 @@ type Querier interface { GetTalkgroupsWithAnyTags(ctx context.Context, tags []string) ([]GetTalkgroupsWithAnyTagsRow, error) GetTalkgroupsWithLearned(ctx context.Context) ([]GetTalkgroupsWithLearnedRow, 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) GetUserByUID(ctx context.Context, id int) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error) diff --git a/pkg/database/talkgroups.sql.go b/pkg/database/talkgroups.sql.go index ff41080..19603d4 100644 --- a/pkg/database/talkgroups.sql.go +++ b/pkg/database/talkgroups.sql.go @@ -377,6 +377,112 @@ func (q *Queries) GetTalkgroupsWithLearnedBySystem(ctx context.Context, system i 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 INSERT INTO talkgroups( system_id, diff --git a/pkg/rest/api.go b/pkg/rest/api.go index ceb014d..8db4f98 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -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 { return &errResponse{ 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{ Err: err, Code: http.StatusNotFound, @@ -86,9 +94,10 @@ func internalError(err error) render.Renderer { type errResponder func(error) render.Renderer var statusMapping = map[error]errResponder{ - tgstore.ErrNoSuchSystem: errTextNotFound, - tgstore.ErrNotFound: errTextNotFound, - pgx.ErrNoRows: recordNotFound, + tgstore.ErrNoSuchSystem: notFoundErrText, + tgstore.ErrNotFound: notFoundErrText, + tgstore.ErrInvalidOrderBy: badRequestErrText, + pgx.ErrNoRows: recordNotFound, } func autoError(err error) render.Renderer { diff --git a/pkg/rest/talkgroups.go b/pkg/rest/talkgroups.go index e1147df..9845764 100644 --- a/pkg/rest/talkgroups.go +++ b/pkg/rest/talkgroups.go @@ -12,6 +12,8 @@ import ( "github.com/go-chi/chi/v5" ) +const DefaultPerPage = 20 + type talkgroupAPI struct { } @@ -19,10 +21,15 @@ func (tga *talkgroupAPI) Subrouter() http.Handler { r := chi.NewMux() 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("/", 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) return r @@ -83,6 +90,42 @@ func (tga *talkgroupAPI) get(w http.ResponseWriter, r *http.Request) { 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) { var id tgParams err := decodeParams(&id, r) diff --git a/pkg/sinks/database.go b/pkg/sinks/database.go index e8529a4..7b01794 100644 --- a/pkg/sinks/database.go +++ b/pkg/sinks/database.go @@ -73,9 +73,9 @@ func (s *DatabaseSink) toAddCallParams(call *calls.Call) database.AddCallParams System: call.System, Talkgroup: call.Talkgroup, CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true}, - AudioName: common.PtrOrNull(call.AudioName), + AudioName: common.NilIfZero(call.AudioName), AudioBlob: call.Audio, - AudioType: common.PtrOrNull(call.AudioType), + AudioType: common.NilIfZero(call.AudioType), Duration: call.Duration.MsInt32Ptr(), Frequency: call.Frequency, Frequencies: call.Frequencies, diff --git a/pkg/sources/http.go b/pkg/sources/http.go index 6255a03..d9fe4e0 100644 --- a/pkg/sources/http.go +++ b/pkg/sources/http.go @@ -83,9 +83,9 @@ func (car *CallUploadRequest) ToCall(submitter auth.UserID) (*calls.Call, error) Frequency: car.Frequency, Frequencies: car.Frequencies, Patches: car.Patches, - TalkgroupLabel: common.PtrOrNull(car.TalkgroupLabel), - TGAlphaTag: common.PtrOrNull(car.TalkgroupTag), - TalkgroupGroup: common.PtrOrNull(car.TalkgroupGroup), + TalkgroupLabel: common.NilIfZero(car.TalkgroupLabel), + TGAlphaTag: common.NilIfZero(car.TalkgroupTag), + TalkgroupGroup: common.NilIfZero(car.TalkgroupGroup), Source: car.Source, }, !car.DontStore) } diff --git a/pkg/talkgroups/tgstore/store.go b/pkg/talkgroups/tgstore/store.go index ccef441..ae4f95c 100644 --- a/pkg/talkgroups/tgstore/store.go +++ b/pkg/talkgroups/tgstore/store.go @@ -21,8 +21,9 @@ import ( type tgMap map[tgsp.ID]*tgsp.Talkgroup var ( - ErrNotFound = errors.New("talkgroup not found") - ErrNoSuchSystem = errors.New("no such system") + ErrNotFound = errors.New("talkgroup not found") + ErrNoSuchSystem = errors.New("no such system") + ErrInvalidOrderBy = errors.New("invalid pagination orderBy value") ) type Store interface { @@ -36,13 +37,13 @@ type Store interface { TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) // 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(ctx context.Context, call *calls.Call) (*tgsp.Talkgroup, error) // 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(ctx context.Context, id int) (string, bool) @@ -63,6 +64,55 @@ type Store interface { 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 const StoreCtxKey storeCtxKey = "store" @@ -148,20 +198,29 @@ func (t *cache) add(rec *tgsp.Talkgroup) { t.Lock() defer t.Unlock() + t.addNoLock(rec) +} + +func (t *cache) addNoLock(rec *tgsp.Talkgroup) { tg := tgsp.TG(rec.System.ID, rec.Talkgroup.TGID) t.tgs[tg] = rec t.systems[int32(rec.System.ID)] = rec.System.Name } -type row interface { +type rowType interface { database.GetTalkgroupsRow | database.GetTalkgroupsWithLearnedRow | - database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow + database.GetTalkgroupsWithLearnedBySystemRow | database.GetTalkgroupWithLearnedRow | + database.GetTalkgroupsWithLearnedBySystemPRow | database.GetTalkgroupsWithLearnedPRow + row +} + +type row interface { GetTalkgroup() database.Talkgroup GetSystem() database.System GetLearned() bool } -func rowToTalkgroup[T row](r T) *tgsp.Talkgroup { +func rowToTalkgroup[T rowType](r T) *tgsp.Talkgroup { return &tgsp.Talkgroup{ Talkgroup: r.GetTalkgroup(), 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 { tg := rowToTalkgroup(rec) - t.add(tg) + t.addNoLock(tg) r = append(r, tg) } @@ -180,8 +242,25 @@ func addToRowList[T row](t *cache, r []*tgsp.Talkgroup, tgRecords []T) []*tgsp.T 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)) + opt := sOpt(opts) var err error if tgs != nil { 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 { return nil, err } - return addToRowList(t, r, tgRecords), nil + return addToRowList(t, tgRecords), nil } // 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 { 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 { @@ -236,14 +325,25 @@ func (t *cache) Weight(ctx context.Context, id tgsp.ID, tm time.Time) float64 { return float64(m) } -func (t *cache) SystemTGs(ctx context.Context, systemID int32) ([]*tgsp.Talkgroup, error) { - recs, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySystem(ctx, systemID) +func (t *cache) SystemTGs(ctx context.Context, systemID int32, opts ...option) ([]*tgsp.Talkgroup, error) { + 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 { return nil, err } - r := make([]*tgsp.Talkgroup, 0, len(recs)) - return addToRowList(t, r, recs), nil + return addToRowList(t, recs), nil } func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) { diff --git a/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go index 31a7e95..9d58745 100644 --- a/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go +++ b/pkg/talkgroups/xport/sdrtrunk/sdrtrunk.go @@ -30,8 +30,8 @@ type Alias struct { func tgToAlias(tg *talkgroups.Talkgroup) Alias { return Alias{ XMLName: xml.Name{Local: "alias"}, - Name: common.ZeroOr(tg.Name), - Group: common.ZeroOr(tg.TGGroup), + Name: common.ZeroIfNil(tg.Name), + Group: common.ZeroIfNil(tg.TGGroup), List: "Stillbox", IDs: []ID{ ID{ diff --git a/sql/postgres/queries/talkgroups.sql b/sql/postgres/queries/talkgroups.sql index 9be53fe..a2be432 100644 --- a/sql/postgres/queries/talkgroups.sql +++ b/sql/postgres/queries/talkgroups.sql @@ -31,6 +31,17 @@ FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id 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 SELECT sqlc.embed(tg), sqlc.embed(sys) @@ -45,6 +56,16 @@ FROM talkgroups tg JOIN systems sys ON tg.system_id = sys.id 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 SELECT name FROM systems WHERE id = @system_id;