2024-12-28 18:32:13 -05:00
|
|
|
package incstore
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-12-29 09:30:24 -05:00
|
|
|
"time"
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
"dynatron.me/x/stillbox/internal/common"
|
|
|
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
2024-12-29 09:30:24 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/calls"
|
2024-12-28 18:32:13 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/database"
|
|
|
|
"dynatron.me/x/stillbox/pkg/incidents"
|
2025-01-18 17:22:08 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/rbac"
|
2025-01-22 10:39:23 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
2025-01-18 17:22:08 -05:00
|
|
|
"dynatron.me/x/stillbox/pkg/users"
|
2024-12-28 18:32:13 -05:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
)
|
|
|
|
|
|
|
|
type IncidentsParams struct {
|
|
|
|
common.Pagination
|
|
|
|
Direction *common.SortDirection `json:"dir"`
|
2024-12-29 19:01:13 -05:00
|
|
|
Filter *string `json:"filter"`
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
Start *jsontypes.Time `json:"start"`
|
|
|
|
End *jsontypes.Time `json:"end"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Store interface {
|
|
|
|
// CreateIncident creates an incident.
|
2024-12-29 16:27:43 -05:00
|
|
|
CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error)
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
// AddToIncident adds the specified call IDs to an incident.
|
|
|
|
// If not nil, notes must be valid json.
|
2024-12-29 09:30:24 -05:00
|
|
|
AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error
|
|
|
|
|
|
|
|
// UpdateNotes updates the notes for a call-incident mapping.
|
|
|
|
UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
// Incidents gets incidents matching parameters and pagination.
|
2024-12-29 19:28:50 -05:00
|
|
|
Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error)
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
// Incident gets a single incident.
|
2024-12-29 09:30:24 -05:00
|
|
|
Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error)
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
// UpdateIncident updates an incident.
|
2024-12-29 16:27:43 -05:00
|
|
|
UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error)
|
2024-12-28 18:32:13 -05:00
|
|
|
|
|
|
|
// DeleteIncident deletes an incident.
|
|
|
|
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
2025-01-19 21:51:39 -05:00
|
|
|
|
|
|
|
// Owner returns an incident with only the owner filled out.
|
|
|
|
Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error)
|
2025-01-20 22:38:27 -05:00
|
|
|
|
|
|
|
// CallIn returns whether an incident is in an call
|
|
|
|
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type store struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
type storeCtxKey string
|
|
|
|
|
|
|
|
const StoreCtxKey storeCtxKey = "store"
|
|
|
|
|
|
|
|
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|
|
|
return context.WithValue(ctx, StoreCtxKey, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func FromCtx(ctx context.Context) Store {
|
|
|
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
|
|
|
if !ok {
|
|
|
|
return NewStore()
|
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewStore() Store {
|
|
|
|
return &store{}
|
|
|
|
}
|
|
|
|
|
2024-12-29 16:27:43 -05:00
|
|
|
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
2025-01-18 17:22:08 -05:00
|
|
|
user, err := users.UserCheck(ctx, new(incidents.Incident), "create")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-12-28 18:32:13 -05:00
|
|
|
db := database.FromCtx(ctx)
|
2024-12-29 16:27:43 -05:00
|
|
|
var dbInc database.Incident
|
2024-12-28 18:32:13 -05:00
|
|
|
|
2024-12-29 16:27:43 -05:00
|
|
|
id := uuid.New()
|
|
|
|
|
|
|
|
txErr := db.InTx(ctx, func(db database.Store) error {
|
|
|
|
var err error
|
|
|
|
dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{
|
|
|
|
ID: id,
|
2025-01-18 17:22:08 -05:00
|
|
|
Owner: user.ID.Int(),
|
2024-12-29 16:27:43 -05:00
|
|
|
Name: inc.Name,
|
|
|
|
Description: inc.Description,
|
|
|
|
StartTime: inc.StartTime.PGTypeTSTZ(),
|
|
|
|
EndTime: inc.EndTime.PGTypeTSTZ(),
|
|
|
|
Location: inc.Location.RawMessage,
|
|
|
|
Metadata: inc.Metadata,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(inc.Calls) > 0 {
|
|
|
|
callIDs := make([]uuid.UUID, 0, len(inc.Calls))
|
|
|
|
notes := make([][]byte, 0, len(inc.Calls))
|
|
|
|
hasNote := false
|
|
|
|
for _, c := range inc.Calls {
|
|
|
|
callIDs = append(callIDs, c.ID)
|
|
|
|
if c.Notes != nil {
|
|
|
|
hasNote = true
|
|
|
|
}
|
|
|
|
notes = append(notes, c.Notes)
|
|
|
|
}
|
|
|
|
if !hasNote {
|
|
|
|
notes = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err = db.AddToIncident(ctx, dbInc.ID, callIDs, notes)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
|
|
|
|
inc = fromDBIncident(id, dbInc)
|
|
|
|
|
|
|
|
return &inc, nil
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
2025-01-19 21:51:39 -05:00
|
|
|
inc, err := s.Owner(ctx, incidentID)
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-01-22 10:39:23 -05:00
|
|
|
_, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionUpdate))
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
return database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
|
|
|
if len(addCallIDs) > 0 {
|
|
|
|
var noteAr [][]byte
|
|
|
|
if notes != nil {
|
|
|
|
noteAr = make([][]byte, len(addCallIDs))
|
|
|
|
for i := range addCallIDs {
|
|
|
|
noteAr[i] = notes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err := db.AddToIncident(ctx, incidentID, addCallIDs, noteAr)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2024-12-28 18:32:13 -05:00
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
if len(removeCallIDs) > 0 {
|
|
|
|
err := db.RemoveFromIncident(ctx, incidentID, removeCallIDs)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
return nil
|
|
|
|
}, pgx.TxOptions{})
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
2024-12-29 19:28:50 -05:00
|
|
|
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
|
2025-01-22 10:39:23 -05:00
|
|
|
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead))
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
2024-12-28 18:32:13 -05:00
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
|
|
|
|
offset, perPage := p.Pagination.OffsetPerPage(100)
|
|
|
|
dbParam := database.ListIncidentsPParams{
|
|
|
|
Start: p.Start.PGTypeTSTZ(),
|
|
|
|
End: p.End.PGTypeTSTZ(),
|
2024-12-29 19:01:13 -05:00
|
|
|
Filter: p.Filter,
|
2024-12-28 18:32:13 -05:00
|
|
|
Direction: p.Direction.DirString(common.DirAsc),
|
|
|
|
Offset: offset,
|
|
|
|
PerPage: perPage,
|
|
|
|
}
|
|
|
|
|
|
|
|
var count int64
|
2024-12-29 19:28:50 -05:00
|
|
|
var rows []database.ListIncidentsPRow
|
2024-12-28 18:32:13 -05:00
|
|
|
txErr := db.InTx(ctx, func(db database.Store) error {
|
|
|
|
var err error
|
2024-12-29 19:04:06 -05:00
|
|
|
count, err = db.ListIncidentsCount(ctx, dbParam.Start, dbParam.End, dbParam.Filter)
|
2024-12-28 18:32:13 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-12-29 15:24:40 -05:00
|
|
|
if offset > int32(count) {
|
|
|
|
return common.ErrPageOutOfRange
|
|
|
|
}
|
|
|
|
|
2024-12-28 18:32:13 -05:00
|
|
|
rows, err = db.ListIncidentsP(ctx, dbParam)
|
|
|
|
return err
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, 0, txErr
|
|
|
|
}
|
|
|
|
|
2024-12-29 19:28:50 -05:00
|
|
|
incs = make([]Incident, 0, len(rows))
|
2024-12-29 15:33:53 -05:00
|
|
|
for _, v := range rows {
|
2024-12-29 19:28:50 -05:00
|
|
|
incs = append(incs, fromDBListInPRow(v.ID, v))
|
2024-12-29 15:33:53 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return incs, int(count), err
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident {
|
|
|
|
return incidents.Incident{
|
|
|
|
ID: id,
|
2025-01-18 17:22:08 -05:00
|
|
|
Owner: users.UserID(d.Owner),
|
2024-12-29 09:30:24 -05:00
|
|
|
Name: d.Name,
|
|
|
|
Description: d.Description,
|
|
|
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
|
|
|
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
|
|
|
Metadata: d.Metadata,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-29 19:28:50 -05:00
|
|
|
type Incident struct {
|
|
|
|
incidents.Incident
|
|
|
|
|
|
|
|
CallCount int `json:"callCount"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident {
|
|
|
|
return Incident{
|
|
|
|
Incident: incidents.Incident{
|
|
|
|
ID: id,
|
2025-01-18 17:22:08 -05:00
|
|
|
Owner: users.UserID(d.Owner),
|
2024-12-29 19:28:50 -05:00
|
|
|
Name: d.Name,
|
|
|
|
Description: d.Description,
|
|
|
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
|
|
|
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
|
|
|
Metadata: d.Metadata,
|
|
|
|
},
|
|
|
|
CallCount: int(d.CallsCount),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
|
|
|
r := make([]incidents.IncidentCall, 0, len(d))
|
|
|
|
for _, v := range d {
|
|
|
|
dur := calls.CallDuration(time.Duration(common.ZeroIfNil(v.Duration)) * time.Millisecond)
|
2025-01-18 17:22:08 -05:00
|
|
|
sub := common.PtrTo(users.UserID(common.ZeroIfNil(v.Submitter)))
|
2024-12-29 09:30:24 -05:00
|
|
|
r = append(r, incidents.IncidentCall{
|
|
|
|
Call: calls.Call{
|
|
|
|
ID: v.CallID,
|
|
|
|
AudioName: common.ZeroIfNil(v.AudioName),
|
|
|
|
AudioType: common.ZeroIfNil(v.AudioType),
|
|
|
|
Duration: dur,
|
|
|
|
DateTime: v.CallDate.Time,
|
|
|
|
Frequencies: v.Frequencies,
|
|
|
|
Frequency: v.Frequency,
|
|
|
|
Patches: v.Patches,
|
|
|
|
Source: v.Source,
|
2025-01-05 10:50:19 -05:00
|
|
|
System: v.SystemID,
|
2024-12-29 09:30:24 -05:00
|
|
|
Submitter: sub,
|
2025-01-05 10:50:19 -05:00
|
|
|
Talkgroup: v.TGID,
|
2024-12-29 09:30:24 -05:00
|
|
|
},
|
|
|
|
Notes: v.Notes,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
2025-01-22 10:39:23 -05:00
|
|
|
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-12-29 09:30:24 -05:00
|
|
|
var r incidents.Incident
|
|
|
|
txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
|
|
|
inc, err := db.GetIncident(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
calls, err := db.GetIncidentCalls(ctx, id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
r = fromDBIncident(id, inc)
|
|
|
|
r.Calls = fromDBCalls(calls)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}, pgx.TxOptions{})
|
|
|
|
if txErr != nil {
|
|
|
|
return nil, txErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return &r, nil
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
type UpdateIncidentParams struct {
|
|
|
|
Name *string `json:"name"`
|
|
|
|
Description *string `json:"description"`
|
|
|
|
StartTime *jsontypes.Time `json:"startTime"`
|
|
|
|
EndTime *jsontypes.Time `json:"endTime"`
|
|
|
|
Location []byte `json:"location"`
|
|
|
|
Metadata jsontypes.Metadata `json:"metadata"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (uip UpdateIncidentParams) toDBUIP(id uuid.UUID) database.UpdateIncidentParams {
|
|
|
|
return database.UpdateIncidentParams{
|
|
|
|
ID: id,
|
|
|
|
Name: uip.Name,
|
|
|
|
Description: uip.Description,
|
|
|
|
StartTime: uip.StartTime.PGTypeTSTZ(),
|
|
|
|
EndTime: uip.EndTime.PGTypeTSTZ(),
|
|
|
|
Location: uip.Location,
|
|
|
|
Metadata: uip.Metadata,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-29 16:27:43 -05:00
|
|
|
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
2025-01-19 21:51:39 -05:00
|
|
|
ckinc, err := s.Owner(ctx, id)
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-01-22 10:39:23 -05:00
|
|
|
_, err = rbac.Check(ctx, &ckinc, rbac.WithActions(entities.ActionUpdate))
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-12-28 18:32:13 -05:00
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
|
2024-12-29 16:27:43 -05:00
|
|
|
dbInc, err := db.UpdateIncident(ctx, p.toDBUIP(id))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
inc := fromDBIncident(id, dbInc)
|
|
|
|
|
|
|
|
return &inc, nil
|
2024-12-28 18:32:13 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
2025-01-19 21:51:39 -05:00
|
|
|
inc, err := s.Owner(ctx, id)
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-01-22 10:39:23 -05:00
|
|
|
_, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionDelete))
|
2025-01-18 17:22:08 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-12-28 18:32:13 -05:00
|
|
|
return database.FromCtx(ctx).DeleteIncident(ctx, id)
|
|
|
|
}
|
2024-12-29 09:30:24 -05:00
|
|
|
|
|
|
|
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
|
|
|
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
|
|
|
|
}
|
2025-01-18 17:22:08 -05:00
|
|
|
|
2025-01-19 21:51:39 -05:00
|
|
|
func (s *store) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) {
|
2025-01-18 17:22:08 -05:00
|
|
|
owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id)
|
|
|
|
return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err
|
|
|
|
}
|
2025-01-20 22:38:27 -05:00
|
|
|
|
|
|
|
func (s *store) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) {
|
|
|
|
db := database.FromCtx(ctx)
|
|
|
|
return db.CallInIncident(ctx, inc, call)
|
|
|
|
}
|