Shares list endpoint

This commit is contained in:
Daniel Ponte 2025-02-10 21:29:32 -05:00
parent 04977b5468
commit d16ad6a4ad
6 changed files with 304 additions and 1 deletions

View file

@ -1924,6 +1924,122 @@ func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (d
return _c return _c
} }
// GetSharesP provides a mock function with given fields: ctx, arg
func (_m *Store) GetSharesP(ctx context.Context, arg database.GetSharesPParams) ([]database.Share, error) {
ret := _m.Called(ctx, arg)
if len(ret) == 0 {
panic("no return value specified for GetSharesP")
}
var r0 []database.Share
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) ([]database.Share, error)); ok {
return rf(ctx, arg)
}
if rf, ok := ret.Get(0).(func(context.Context, database.GetSharesPParams) []database.Share); ok {
r0 = rf(ctx, arg)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.Share)
}
}
if rf, ok := ret.Get(1).(func(context.Context, database.GetSharesPParams) error); ok {
r1 = rf(ctx, arg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetSharesP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesP'
type Store_GetSharesP_Call struct {
*mock.Call
}
// GetSharesP is a helper method to define mock.On call
// - ctx context.Context
// - arg database.GetSharesPParams
func (_e *Store_Expecter) GetSharesP(ctx interface{}, arg interface{}) *Store_GetSharesP_Call {
return &Store_GetSharesP_Call{Call: _e.mock.On("GetSharesP", ctx, arg)}
}
func (_c *Store_GetSharesP_Call) Run(run func(ctx context.Context, arg database.GetSharesPParams)) *Store_GetSharesP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(database.GetSharesPParams))
})
return _c
}
func (_c *Store_GetSharesP_Call) Return(_a0 []database.Share, _a1 error) *Store_GetSharesP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetSharesP_Call) RunAndReturn(run func(context.Context, database.GetSharesPParams) ([]database.Share, error)) *Store_GetSharesP_Call {
_c.Call.Return(run)
return _c
}
// GetSharesPCount provides a mock function with given fields: ctx, owner
func (_m *Store) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) {
ret := _m.Called(ctx, owner)
if len(ret) == 0 {
panic("no return value specified for GetSharesPCount")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *int32) (int64, error)); ok {
return rf(ctx, owner)
}
if rf, ok := ret.Get(0).(func(context.Context, *int32) int64); ok {
r0 = rf(ctx, owner)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, *int32) error); ok {
r1 = rf(ctx, owner)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetSharesPCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSharesPCount'
type Store_GetSharesPCount_Call struct {
*mock.Call
}
// GetSharesPCount is a helper method to define mock.On call
// - ctx context.Context
// - owner *int32
func (_e *Store_Expecter) GetSharesPCount(ctx interface{}, owner interface{}) *Store_GetSharesPCount_Call {
return &Store_GetSharesPCount_Call{Call: _e.mock.On("GetSharesPCount", ctx, owner)}
}
func (_c *Store_GetSharesPCount_Call) Run(run func(ctx context.Context, owner *int32)) *Store_GetSharesPCount_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(*int32))
})
return _c
}
func (_c *Store_GetSharesPCount_Call) Return(_a0 int64, _a1 error) *Store_GetSharesPCount_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetSharesPCount_Call) RunAndReturn(run func(context.Context, *int32) (int64, error)) *Store_GetSharesPCount_Call {
_c.Call.Return(run)
return _c
}
// GetSystemName provides a mock function with given fields: ctx, systemID // GetSystemName provides a mock function with given fields: ctx, systemID
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) { func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
ret := _m.Called(ctx, systemID) ret := _m.Called(ctx, systemID)

View file

@ -42,6 +42,8 @@ type Querier interface {
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error)
GetShare(ctx context.Context, id string) (Share, error) GetShare(ctx context.Context, id string) (Share, error)
GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error)
GetSharesPCount(ctx context.Context, owner *int32) (int64, error)
GetSystemName(ctx context.Context, systemID int) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error)
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error) GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)

View file

@ -79,6 +79,79 @@ func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
return i, err return i, err
} }
const getSharesP = `-- name: GetSharesP :many
SELECT
s.id,
s.entity_type,
s.entity_id,
s.entity_date,
s.owner,
s.expiration
FROM shares s
WHERE
CASE WHEN $1::INTEGER IS NOT NULL THEN
s.owner = $1 ELSE TRUE END
ORDER BY
CASE WHEN $2::TEXT = 'asc' THEN s.entity_date END ASC,
CASE WHEN $2::TEXT = 'desc' THEN s.entity_date END DESC
OFFSET $3 ROWS
FETCH NEXT $4 ROWS ONLY
`
type GetSharesPParams struct {
Owner *int32 `json:"owner"`
Direction string `json:"direction"`
Offset int32 `json:"offset"`
PerPage int32 `json:"perPage"`
}
func (q *Queries) GetSharesP(ctx context.Context, arg GetSharesPParams) ([]Share, error) {
rows, err := q.db.Query(ctx, getSharesP,
arg.Owner,
arg.Direction,
arg.Offset,
arg.PerPage,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Share
for rows.Next() {
var i Share
if err := rows.Scan(
&i.ID,
&i.EntityType,
&i.EntityID,
&i.EntityDate,
&i.Owner,
&i.Expiration,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getSharesPCount = `-- name: GetSharesPCount :one
SELECT COUNT(*)
FROM shares s
WHERE
CASE WHEN $1::INTEGER IS NOT NULL THEN
s.owner = $1 ELSE TRUE END
`
func (q *Queries) GetSharesPCount(ctx context.Context, owner *int32) (int64, error) {
row := q.db.QueryRow(ctx, getSharesPCount, owner)
var count int64
err := row.Scan(&count)
return count, err
}
const pruneShares = `-- name: PruneShares :exec const pruneShares = `-- name: PruneShares :exec
DELETE FROM shares WHERE expiration < NOW() DELETE FROM shares WHERE expiration < NOW()
` `

View file

@ -121,6 +121,7 @@ func (sa *shareAPI) Subrouter() http.Handler {
r.Post(`/create`, sa.createShare) r.Post(`/create`, sa.createShare)
r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare) r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare)
r.Post(`/`, sa.listShares)
return r return r
} }
@ -156,6 +157,34 @@ func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
respond(w, r, sh) respond(w, r, sh)
} }
func (sa *shareAPI) listShares(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
shs := shares.FromCtx(ctx)
p := shares.SharesParams{}
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
if err != nil {
wErr(w, r, badRequest(err))
return
}
shRes, count, err := shs.Shares(ctx, p)
if err != nil {
wErr(w, r, autoError(err))
return
}
response := struct {
Shares []*shares.Share `json:"shares"`
TotalCount int `json:"totalCount"`
}{
Shares: shRes,
TotalCount: count,
}
respond(w, r, &response)
}
func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
shs := shares.FromCtx(ctx) shs := shares.FromCtx(ctx)

View file

@ -3,7 +3,9 @@ package shares
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
@ -12,13 +14,21 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
type SharesParams struct {
common.Pagination
Direction *common.SortDirection `json:"dir"`
}
type Shares interface { type Shares interface {
// NewShare creates a new share. // NewShare creates a new share.
NewShare(ctx context.Context, sh CreateShareParams) (*Share, error) NewShare(ctx context.Context, sh CreateShareParams) (*Share, error)
// Share retreives a share record. // Share retrieves a share record.
GetShare(ctx context.Context, id string) (*Share, error) GetShare(ctx context.Context, id string) (*Share, error)
// Shares retrieves shares visible by the context Subject.
Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error)
// Create stores a new share record. // Create stores a new share record.
Create(ctx context.Context, share *Share) error Create(ctx context.Context, share *Share) error
@ -98,6 +108,52 @@ func (s *postgresStore) Delete(ctx context.Context, id string) error {
return database.FromCtx(ctx).DeleteShare(ctx, id) return database.FromCtx(ctx).DeleteShare(ctx, id)
} }
func (s *postgresStore) Shares(ctx context.Context, p SharesParams) (shares []*Share, totalCount int, err error) {
sub := entities.SubjectFrom(ctx)
// ersatz RBAC
owner := common.PtrTo(int32(-1)) // invalid UID
switch s := sub.(type) {
case *users.User:
if !s.IsAdmin {
owner = s.ID.Int32Ptr()
} else {
owner = nil
}
case *entities.SystemServiceSubject:
owner = nil
default:
return nil, 0, rbac.ErrAccessDenied(rbac.ErrNotAuthorized)
}
db := database.FromCtx(ctx)
count, err := db.GetSharesPCount(ctx, owner)
if err != nil {
return nil, 0, fmt.Errorf("shares count: %w", err)
}
offset, perPage := p.Pagination.OffsetPerPage(100)
dbParam := database.GetSharesPParams{
Owner: owner,
Direction: p.Direction.DirString(common.DirAsc),
Offset: offset,
PerPage: perPage,
}
shs, err := db.GetSharesP(ctx, dbParam)
if err != nil {
return nil, 0, err
}
shares = make([]*Share, 0, len(shs))
for _, v := range shs {
shares = append(shares, recToShare(v))
}
return shares, int(count), nil
}
func (s *postgresStore) Prune(ctx context.Context) error { func (s *postgresStore) Prune(ctx context.Context) error {
return database.FromCtx(ctx).PruneShares(ctx) return database.FromCtx(ctx).PruneShares(ctx)
} }

View file

@ -24,3 +24,30 @@ DELETE FROM shares WHERE id = @id;
-- name: PruneShares :exec -- name: PruneShares :exec
DELETE FROM shares WHERE expiration < NOW(); DELETE FROM shares WHERE expiration < NOW();
-- name: GetSharesP :many
SELECT
s.id,
s.entity_type,
s.entity_id,
s.entity_date,
s.owner,
s.expiration
FROM shares s
WHERE
CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN
s.owner = @owner ELSE TRUE END
ORDER BY
CASE WHEN @direction::TEXT = 'asc' THEN s.entity_date END ASC,
CASE WHEN @direction::TEXT = 'desc' THEN s.entity_date END DESC
OFFSET sqlc.arg('offset') ROWS
FETCH NEXT sqlc.arg('per_page') ROWS ONLY
;
-- name: GetSharesPCount :one
SELECT COUNT(*)
FROM shares s
WHERE
CASE WHEN sqlc.narg('owner')::INTEGER IS NOT NULL THEN
s.owner = @owner ELSE TRUE END
;