Incidents

This commit is contained in:
Daniel Ponte 2024-12-28 18:32:13 -05:00
parent 7acb89ce6c
commit cac95e1b25
14 changed files with 1308 additions and 40 deletions

View file

@ -0,0 +1,9 @@
package jsontypes
import (
"encoding/json"
)
type Location struct {
json.RawMessage
}

View file

@ -26,7 +26,7 @@ type Store interface {
type store struct {
}
func New() *store {
func NewStore() *store {
return new(store)
}
@ -41,7 +41,7 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
func FromCtx(ctx context.Context) Store {
s, ok := ctx.Value(StoreCtxKey).(Store)
if !ok {
return New()
return NewStore()
}
return s

View file

@ -0,0 +1,301 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: incidents.sql
package database
import (
"context"
"dynatron.me/x/stillbox/internal/jsontypes"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const addToIncident = `-- name: AddToIncident :exec
WITH inp AS (
SELECT
UNNEST($2::UUID[]) id,
UNNEST($3::JSONB[]) notes
) INSERT INTO incidents_calls(
incident_id,
call_id,
calls_tbl_id,
call_date,
notes
)
SELECT
$1::UUID,
inp.id,
inp.id,
c.call_date,
inp.notes
FROM inp
JOIN calls c ON c.id = inp.id
`
func (q *Queries) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error {
_, err := q.db.Exec(ctx, addToIncident, incidentID, callIds, notes)
return err
}
const createIncident = `-- name: CreateIncident :one
INSERT INTO incidents (
id,
name,
description,
start_time,
end_time,
location,
metadata
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
)
RETURNING id, name, description, start_time, end_time, location, metadata
`
type CreateIncidentParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
StartTime pgtype.Timestamptz `json:"start_time"`
EndTime pgtype.Timestamptz `json:"end_time"`
Location []byte `json:"location"`
Metadata jsontypes.Metadata `json:"metadata"`
}
func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error) {
row := q.db.QueryRow(ctx, createIncident,
arg.ID,
arg.Name,
arg.Description,
arg.StartTime,
arg.EndTime,
arg.Location,
arg.Metadata,
)
var i Incident
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.StartTime,
&i.EndTime,
&i.Location,
&i.Metadata,
)
return i, err
}
const deleteIncident = `-- name: DeleteIncident :exec
DELETE FROM incidents CASCADE WHERE id = $1
`
func (q *Queries) DeleteIncident(ctx context.Context, id uuid.UUID) error {
_, err := q.db.Exec(ctx, deleteIncident, id)
return err
}
const getIncident = `-- name: GetIncident :one
SELECT
i.id,
i.name,
i.description,
i.start_time,
i.end_time,
i.location,
i.metadata
FROM incidents i
WHERE i.id = $1
`
func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) {
row := q.db.QueryRow(ctx, getIncident, id)
var i Incident
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.StartTime,
&i.EndTime,
&i.Location,
&i.Metadata,
)
return i, err
}
const incidentCalls = `-- name: IncidentCalls :many
SELECT
ic.incident_id, call_date,
ic.call_id,
ic.notes
FROM incidents_calls ic
`
type IncidentCallsRow struct {
IncidentID uuid.UUID `json:"incident_id"`
CallDate pgtype.Timestamptz `json:"call_date"`
CallID uuid.UUID `json:"call_id"`
Notes []byte `json:"notes"`
}
// INCOMPLETE
func (q *Queries) IncidentCalls(ctx context.Context) ([]IncidentCallsRow, error) {
rows, err := q.db.Query(ctx, incidentCalls)
if err != nil {
return nil, err
}
defer rows.Close()
var items []IncidentCallsRow
for rows.Next() {
var i IncidentCallsRow
if err := rows.Scan(
&i.IncidentID,
&i.CallDate,
&i.CallID,
&i.Notes,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listIncidentsCount = `-- name: ListIncidentsCount :one
SELECT COUNT(*)
FROM incidents i
WHERE
CASE WHEN $1::TIMESTAMPTZ IS NOT NULL THEN
i.start_time >= $1 ELSE TRUE END AND
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
i.start_time <= $2 ELSE TRUE END
`
func (q *Queries) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz) (int64, error) {
row := q.db.QueryRow(ctx, listIncidentsCount, start, end)
var count int64
err := row.Scan(&count)
return count, err
}
const listIncidentsP = `-- name: ListIncidentsP :many
SELECT
i.id,
i.name,
i.description,
i.start_time,
i.end_time,
i.location,
i.metadata
FROM incidents i
WHERE
CASE WHEN $1::TIMESTAMPTZ IS NOT NULL THEN
i.start_time >= $1 ELSE TRUE END AND
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
i.start_time <= $2 ELSE TRUE END
ORDER BY
CASE WHEN $3::TEXT = 'asc' THEN i.start_time END ASC,
CASE WHEN $3::TEXT = 'desc' THEN i.start_time END DESC
OFFSET $4 ROWS
FETCH NEXT $5 ROWS ONLY
`
type ListIncidentsPParams struct {
Start pgtype.Timestamptz `json:"start"`
End pgtype.Timestamptz `json:"end"`
Direction string `json:"direction"`
Offset int32 `json:"offset"`
PerPage int32 `json:"per_page"`
}
func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]Incident, error) {
rows, err := q.db.Query(ctx, listIncidentsP,
arg.Start,
arg.End,
arg.Direction,
arg.Offset,
arg.PerPage,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Incident
for rows.Next() {
var i Incident
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.StartTime,
&i.EndTime,
&i.Location,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateIncident = `-- name: UpdateIncident :one
UPDATE incidents
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
start_time = COALESCE($3, start_time),
end_time = COALESCE($4, end_time),
location = COALESCE($5, location),
metadata = COALESCE($6, metadata)
WHERE
id = $7
RETURNING id, name, description, start_time, end_time, location, metadata
`
type UpdateIncidentParams struct {
Name *string `json:"name"`
Description *string `json:"description"`
StartTime pgtype.Timestamptz `json:"start_time"`
EndTime pgtype.Timestamptz `json:"end_time"`
Location []byte `json:"location"`
Metadata jsontypes.Metadata `json:"metadata"`
ID uuid.UUID `json:"id"`
}
func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error) {
row := q.db.QueryRow(ctx, updateIncident,
arg.Name,
arg.Description,
arg.StartTime,
arg.EndTime,
arg.Location,
arg.Metadata,
arg.ID,
)
var i Incident
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.StartTime,
&i.EndTime,
&i.Location,
&i.Metadata,
)
return i, err
}

View file

@ -181,6 +181,55 @@ func (_c *Store_AddLearnedTalkgroup_Call) RunAndReturn(run func(context.Context,
return _c
}
// AddToIncident provides a mock function with given fields: ctx, incidentID, callIds, notes
func (_m *Store) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error {
ret := _m.Called(ctx, incidentID, callIds, notes)
if len(ret) == 0 {
panic("no return value specified for AddToIncident")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, []uuid.UUID, [][]byte) error); ok {
r0 = rf(ctx, incidentID, callIds, notes)
} else {
r0 = ret.Error(0)
}
return r0
}
// Store_AddToIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddToIncident'
type Store_AddToIncident_Call struct {
*mock.Call
}
// AddToIncident is a helper method to define mock.On call
// - ctx context.Context
// - incidentID uuid.UUID
// - callIds []uuid.UUID
// - notes [][]byte
func (_e *Store_Expecter) AddToIncident(ctx interface{}, incidentID interface{}, callIds interface{}, notes interface{}) *Store_AddToIncident_Call {
return &Store_AddToIncident_Call{Call: _e.mock.On("AddToIncident", ctx, incidentID, callIds, notes)}
}
func (_c *Store_AddToIncident_Call) Run(run func(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte)) *Store_AddToIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID), args[2].([]uuid.UUID), args[3].([][]byte))
})
return _c
}
func (_c *Store_AddToIncident_Call) Return(_a0 error) *Store_AddToIncident_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Store_AddToIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, []uuid.UUID, [][]byte) error) *Store_AddToIncident_Call {
_c.Call.Return(run)
return _c
}
// BulkSetTalkgroupTags provides a mock function with given fields: ctx, tgs, tags
func (_m *Store) BulkSetTalkgroupTags(ctx context.Context, tgs database.TGTuples, tags []string) error {
ret := _m.Called(ctx, tgs, tags)
@ -346,6 +395,63 @@ func (_c *Store_CreateAPIKey_Call) RunAndReturn(run func(context.Context, int, p
return _c
}
// CreateIncident provides a mock function with given fields: ctx, arg
func (_m *Store) CreateIncident(ctx context.Context, arg database.CreateIncidentParams) (database.Incident, error) {
ret := _m.Called(ctx, arg)
if len(ret) == 0 {
panic("no return value specified for CreateIncident")
}
var r0 database.Incident
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, database.CreateIncidentParams) (database.Incident, error)); ok {
return rf(ctx, arg)
}
if rf, ok := ret.Get(0).(func(context.Context, database.CreateIncidentParams) database.Incident); ok {
r0 = rf(ctx, arg)
} else {
r0 = ret.Get(0).(database.Incident)
}
if rf, ok := ret.Get(1).(func(context.Context, database.CreateIncidentParams) error); ok {
r1 = rf(ctx, arg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_CreateIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateIncident'
type Store_CreateIncident_Call struct {
*mock.Call
}
// CreateIncident is a helper method to define mock.On call
// - ctx context.Context
// - arg database.CreateIncidentParams
func (_e *Store_Expecter) CreateIncident(ctx interface{}, arg interface{}) *Store_CreateIncident_Call {
return &Store_CreateIncident_Call{Call: _e.mock.On("CreateIncident", ctx, arg)}
}
func (_c *Store_CreateIncident_Call) Run(run func(ctx context.Context, arg database.CreateIncidentParams)) *Store_CreateIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(database.CreateIncidentParams))
})
return _c
}
func (_c *Store_CreateIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_CreateIncident_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_CreateIncident_Call) RunAndReturn(run func(context.Context, database.CreateIncidentParams) (database.Incident, error)) *Store_CreateIncident_Call {
_c.Call.Return(run)
return _c
}
// CreatePartition provides a mock function with given fields: ctx, parentTable, partitionName, start, end
func (_m *Store) CreatePartition(ctx context.Context, parentTable string, partitionName string, start time.Time, end time.Time) error {
ret := _m.Called(ctx, parentTable, partitionName, start, end)
@ -642,6 +748,53 @@ func (_c *Store_DeleteAPIKey_Call) RunAndReturn(run func(context.Context, string
return _c
}
// DeleteIncident provides a mock function with given fields: ctx, id
func (_m *Store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for DeleteIncident")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Store_DeleteIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteIncident'
type Store_DeleteIncident_Call struct {
*mock.Call
}
// DeleteIncident is a helper method to define mock.On call
// - ctx context.Context
// - id uuid.UUID
func (_e *Store_Expecter) DeleteIncident(ctx interface{}, id interface{}) *Store_DeleteIncident_Call {
return &Store_DeleteIncident_Call{Call: _e.mock.On("DeleteIncident", ctx, id)}
}
func (_c *Store_DeleteIncident_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_DeleteIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID))
})
return _c
}
func (_c *Store_DeleteIncident_Call) Return(_a0 error) *Store_DeleteIncident_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID) error) *Store_DeleteIncident_Call {
_c.Call.Return(run)
return _c
}
// DeleteSystem provides a mock function with given fields: ctx, id
func (_m *Store) DeleteSystem(ctx context.Context, id int) error {
ret := _m.Called(ctx, id)
@ -1167,6 +1320,63 @@ func (_c *Store_GetDatabaseSize_Call) RunAndReturn(run func(context.Context) (st
return _c
}
// GetIncident provides a mock function with given fields: ctx, id
func (_m *Store) GetIncident(ctx context.Context, id uuid.UUID) (database.Incident, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetIncident")
}
var r0 database.Incident
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.Incident, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.Incident); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(database.Incident)
}
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncident'
type Store_GetIncident_Call struct {
*mock.Call
}
// GetIncident is a helper method to define mock.On call
// - ctx context.Context
// - id uuid.UUID
func (_e *Store_Expecter) GetIncident(ctx interface{}, id interface{}) *Store_GetIncident_Call {
return &Store_GetIncident_Call{Call: _e.mock.On("GetIncident", ctx, id)}
}
func (_c *Store_GetIncident_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID))
})
return _c
}
func (_c *Store_GetIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_GetIncident_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID) (database.Incident, error)) *Store_GetIncident_Call {
_c.Call.Return(run)
return _c
}
// GetSystemName provides a mock function with given fields: ctx, systemID
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
ret := _m.Called(ctx, systemID)
@ -2384,6 +2594,64 @@ func (_c *Store_InTx_Call) RunAndReturn(run func(context.Context, func(database.
return _c
}
// IncidentCalls provides a mock function with given fields: ctx
func (_m *Store) IncidentCalls(ctx context.Context) ([]database.IncidentCallsRow, error) {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for IncidentCalls")
}
var r0 []database.IncidentCallsRow
var r1 error
if rf, ok := ret.Get(0).(func(context.Context) ([]database.IncidentCallsRow, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context) []database.IncidentCallsRow); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.IncidentCallsRow)
}
}
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_IncidentCalls_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncidentCalls'
type Store_IncidentCalls_Call struct {
*mock.Call
}
// IncidentCalls is a helper method to define mock.On call
// - ctx context.Context
func (_e *Store_Expecter) IncidentCalls(ctx interface{}) *Store_IncidentCalls_Call {
return &Store_IncidentCalls_Call{Call: _e.mock.On("IncidentCalls", ctx)}
}
func (_c *Store_IncidentCalls_Call) Run(run func(ctx context.Context)) *Store_IncidentCalls_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *Store_IncidentCalls_Call) Return(_a0 []database.IncidentCallsRow, _a1 error) *Store_IncidentCalls_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_IncidentCalls_Call) RunAndReturn(run func(context.Context) ([]database.IncidentCallsRow, error)) *Store_IncidentCalls_Call {
_c.Call.Return(run)
return _c
}
// ListCallsCount provides a mock function with given fields: ctx, arg
func (_m *Store) ListCallsCount(ctx context.Context, arg database.ListCallsCountParams) (int64, error) {
ret := _m.Called(ctx, arg)
@ -2500,6 +2768,123 @@ func (_c *Store_ListCallsP_Call) RunAndReturn(run func(context.Context, database
return _c
}
// ListIncidentsCount provides a mock function with given fields: ctx, start, end
func (_m *Store) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz) (int64, error) {
ret := _m.Called(ctx, start, end)
if len(ret) == 0 {
panic("no return value specified for ListIncidentsCount")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz) (int64, error)); ok {
return rf(ctx, start, end)
}
if rf, ok := ret.Get(0).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz) int64); ok {
r0 = rf(ctx, start, end)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz) error); ok {
r1 = rf(ctx, start, end)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_ListIncidentsCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListIncidentsCount'
type Store_ListIncidentsCount_Call struct {
*mock.Call
}
// ListIncidentsCount is a helper method to define mock.On call
// - ctx context.Context
// - start pgtype.Timestamptz
// - end pgtype.Timestamptz
func (_e *Store_Expecter) ListIncidentsCount(ctx interface{}, start interface{}, end interface{}) *Store_ListIncidentsCount_Call {
return &Store_ListIncidentsCount_Call{Call: _e.mock.On("ListIncidentsCount", ctx, start, end)}
}
func (_c *Store_ListIncidentsCount_Call) Run(run func(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz)) *Store_ListIncidentsCount_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(pgtype.Timestamptz), args[2].(pgtype.Timestamptz))
})
return _c
}
func (_c *Store_ListIncidentsCount_Call) Return(_a0 int64, _a1 error) *Store_ListIncidentsCount_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_ListIncidentsCount_Call) RunAndReturn(run func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz) (int64, error)) *Store_ListIncidentsCount_Call {
_c.Call.Return(run)
return _c
}
// ListIncidentsP provides a mock function with given fields: ctx, arg
func (_m *Store) ListIncidentsP(ctx context.Context, arg database.ListIncidentsPParams) ([]database.Incident, error) {
ret := _m.Called(ctx, arg)
if len(ret) == 0 {
panic("no return value specified for ListIncidentsP")
}
var r0 []database.Incident
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, database.ListIncidentsPParams) ([]database.Incident, error)); ok {
return rf(ctx, arg)
}
if rf, ok := ret.Get(0).(func(context.Context, database.ListIncidentsPParams) []database.Incident); ok {
r0 = rf(ctx, arg)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.Incident)
}
}
if rf, ok := ret.Get(1).(func(context.Context, database.ListIncidentsPParams) error); ok {
r1 = rf(ctx, arg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_ListIncidentsP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListIncidentsP'
type Store_ListIncidentsP_Call struct {
*mock.Call
}
// ListIncidentsP is a helper method to define mock.On call
// - ctx context.Context
// - arg database.ListIncidentsPParams
func (_e *Store_Expecter) ListIncidentsP(ctx interface{}, arg interface{}) *Store_ListIncidentsP_Call {
return &Store_ListIncidentsP_Call{Call: _e.mock.On("ListIncidentsP", ctx, arg)}
}
func (_c *Store_ListIncidentsP_Call) Run(run func(ctx context.Context, arg database.ListIncidentsPParams)) *Store_ListIncidentsP_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(database.ListIncidentsPParams))
})
return _c
}
func (_c *Store_ListIncidentsP_Call) Return(_a0 []database.Incident, _a1 error) *Store_ListIncidentsP_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, database.ListIncidentsPParams) ([]database.Incident, error)) *Store_ListIncidentsP_Call {
_c.Call.Return(run)
return _c
}
// RestoreTalkgroupVersion provides a mock function with given fields: ctx, versionIds
func (_m *Store) RestoreTalkgroupVersion(ctx context.Context, versionIds int) (database.Talkgroup, error) {
ret := _m.Called(ctx, versionIds)
@ -2859,6 +3244,63 @@ func (_c *Store_SweepCalls_Call) RunAndReturn(run func(context.Context, pgtype.T
return _c
}
// UpdateIncident provides a mock function with given fields: ctx, arg
func (_m *Store) UpdateIncident(ctx context.Context, arg database.UpdateIncidentParams) (database.Incident, error) {
ret := _m.Called(ctx, arg)
if len(ret) == 0 {
panic("no return value specified for UpdateIncident")
}
var r0 database.Incident
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, database.UpdateIncidentParams) (database.Incident, error)); ok {
return rf(ctx, arg)
}
if rf, ok := ret.Get(0).(func(context.Context, database.UpdateIncidentParams) database.Incident); ok {
r0 = rf(ctx, arg)
} else {
r0 = ret.Get(0).(database.Incident)
}
if rf, ok := ret.Get(1).(func(context.Context, database.UpdateIncidentParams) error); ok {
r1 = rf(ctx, arg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_UpdateIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateIncident'
type Store_UpdateIncident_Call struct {
*mock.Call
}
// UpdateIncident is a helper method to define mock.On call
// - ctx context.Context
// - arg database.UpdateIncidentParams
func (_e *Store_Expecter) UpdateIncident(ctx interface{}, arg interface{}) *Store_UpdateIncident_Call {
return &Store_UpdateIncident_Call{Call: _e.mock.On("UpdateIncident", ctx, arg)}
}
func (_c *Store_UpdateIncident_Call) Run(run func(ctx context.Context, arg database.UpdateIncidentParams)) *Store_UpdateIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(database.UpdateIncidentParams))
})
return _c
}
func (_c *Store_UpdateIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_UpdateIncident_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_UpdateIncident_Call) RunAndReturn(run func(context.Context, database.UpdateIncidentParams) (database.Incident, error)) *Store_UpdateIncident_Call {
_c.Call.Return(run)
return _c
}
// UpdatePassword provides a mock function with given fields: ctx, username, password
func (_m *Store) UpdatePassword(ctx context.Context, username string, password string) error {
ret := _m.Called(ctx, username, password)

View file

@ -59,10 +59,10 @@ type Incident struct {
ID uuid.UUID `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
StartTime pgtype.Timestamp `json:"start_time,omitempty"`
EndTime pgtype.Timestamp `json:"end_time,omitempty"`
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
Location []byte `json:"location,omitempty"`
Metadata []byte `json:"metadata,omitempty"`
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
}
type IncidentsCall struct {

View file

@ -15,12 +15,15 @@ type Querier interface {
AddAlert(ctx context.Context, arg AddAlertParams) error
AddCall(ctx context.Context, arg AddCallParams) error
AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error)
AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error
// This is used to sweep calls that are part of an incident prior to pruning a partition.
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
CreateSystem(ctx context.Context, iD int, name string) error
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteAPIKey(ctx context.Context, apiKey string) error
DeleteIncident(ctx context.Context, id uuid.UUID) error
DeleteSystem(ctx context.Context, id int) error
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
DeleteUser(ctx context.Context, username string) error
@ -29,6 +32,7 @@ type Querier interface {
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
GetDatabaseSize(ctx context.Context) (string, error)
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
GetSystemName(ctx context.Context, systemID int) (string, error)
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
@ -46,8 +50,12 @@ type Querier interface {
GetUserByUID(ctx context.Context, id int) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error)
GetUsers(ctx context.Context) ([]User, error)
// INCOMPLETE
IncidentCalls(ctx context.Context) ([]IncidentCallsRow, error)
ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error)
ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error)
ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz) (int64, error)
ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]Incident, error)
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
@ -55,6 +63,7 @@ type Querier interface {
StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error
StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults
SweepCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error)
UpdatePassword(ctx context.Context, username string, password string) error
UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
UpsertTalkgroup(ctx context.Context, arg []UpsertTalkgroupParams) *UpsertTalkgroupBatchResults

16
pkg/incidents/incident.go Normal file
View file

@ -0,0 +1,16 @@
package incidents
import (
"dynatron.me/x/stillbox/internal/jsontypes"
"github.com/google/uuid"
)
type Incident struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
StartTime *jsontypes.Time `json:"startTime"`
EndTime *jsontypes.Time `json:"endTime"`
Location jsontypes.Location `json:"location"`
Metadata jsontypes.Metadata `json:"metadata"`
}

View file

@ -0,0 +1,159 @@
package incstore
import (
"context"
"dynatron.me/x/stillbox/internal/common"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/incidents"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
type IncidentsParams struct {
common.Pagination
Direction *common.SortDirection `json:"dir"`
Start *jsontypes.Time `json:"start"`
End *jsontypes.Time `json:"end"`
}
type Store interface {
// CreateIncident creates an incident.
CreateIncident(ctx context.Context, inc incidents.Incident) (database.Incident, error)
// AddToIncident adds the specified call IDs to an incident.
// If not nil, notes must be valid json.
AddToIncident(ctx context.Context, incidentID uuid.UUID, callIDs []uuid.UUID, notes []byte) error
// Incidents gets incidents matching parameters and pagination.
Incidents(ctx context.Context, p IncidentsParams) (incs []database.Incident, totalCount int, err error)
// Incident gets a single incident.
Incident(ctx context.Context, id uuid.UUID) (database.Incident, error)
// UpdateIncident updates an incident.
UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (database.Incident, error)
// DeleteIncident deletes an incident.
DeleteIncident(ctx context.Context, id uuid.UUID) error
}
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{}
}
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (database.Incident, error) {
db := database.FromCtx(ctx)
dbInc, err := db.CreateIncident(ctx, database.CreateIncidentParams{
ID: uuid.New(),
Name: inc.Name,
Description: inc.Description,
StartTime: inc.StartTime.PGTypeTSTZ(),
EndTime: inc.EndTime.PGTypeTSTZ(),
Location: inc.Location.RawMessage,
Metadata: inc.Metadata,
})
return dbInc, err
}
func (s *store) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIDs []uuid.UUID, notes []byte) error {
db := database.FromCtx(ctx)
var noteAr [][]byte
if notes != nil {
noteAr = make([][]byte, len(callIDs))
for i := range callIDs {
noteAr[i] = notes
}
}
return db.AddToIncident(ctx, incidentID, callIDs, noteAr)
}
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (rows []database.Incident, totalCount int, err error) {
db := database.FromCtx(ctx)
offset, perPage := p.Pagination.OffsetPerPage(100)
dbParam := database.ListIncidentsPParams{
Start: p.Start.PGTypeTSTZ(),
End: p.End.PGTypeTSTZ(),
Direction: p.Direction.DirString(common.DirAsc),
Offset: offset,
PerPage: perPage,
}
var count int64
txErr := db.InTx(ctx, func(db database.Store) error {
var err error
count, err = db.ListIncidentsCount(ctx, dbParam.Start, dbParam.End)
if err != nil {
return err
}
rows, err = db.ListIncidentsP(ctx, dbParam)
return err
}, pgx.TxOptions{})
if txErr != nil {
return nil, 0, txErr
}
return rows, int(count), err
}
func (s *store) Incident(ctx context.Context, id uuid.UUID) (database.Incident, error) {
return database.FromCtx(ctx).GetIncident(ctx, id)
}
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,
}
}
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (database.Incident, error) {
db := database.FromCtx(ctx)
return db.UpdateIncident(ctx, p.toDBUIP(id))
}
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
return database.FromCtx(ctx).DeleteIncident(ctx, id)
}

View file

@ -32,6 +32,7 @@ func (a *api) Subrouter() http.Handler {
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
r.Mount("/call", new(callsAPI).Subrouter())
r.Mount("/user", new(usersAPI).Subrouter())
r.Mount("/incident", new(incidentsAPI).Subrouter())
return r
}

152
pkg/rest/incidents.go Normal file
View file

@ -0,0 +1,152 @@
package rest
import (
"net/http"
"dynatron.me/x/stillbox/internal/forms"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/incidents"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type incidentsAPI struct {
}
func (ia *incidentsAPI) Subrouter() http.Handler {
r := chi.NewMux()
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident)
r.Post(`/create`, ia.createIncident)
r.Post(`/`, ia.listIncidents)
r.Put(`/{id:[a-f0-9]+}`, ia.updateIncident)
r.Delete(`/{id:[a-f0-9]+}`, ia.deleteIncident)
return r
}
func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
p := incstore.IncidentsParams{}
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
if err != nil {
wErr(w, r, badRequest(err))
return
}
res := struct {
Incidents []database.Incident `json:"incidents"`
Count int `json:"count"`
}{}
res.Incidents, res.Count, err = incs.Incidents(ctx, p)
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, res)
}
func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
p := incidents.Incident{}
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
if err != nil {
wErr(w, r, badRequest(err))
return
}
inc, err := incs.CreateIncident(ctx, p)
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, inc)
}
func (ia *incidentsAPI) getIncident(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
params := struct {
ID uuid.UUID `param:"id"`
}{}
err := decodeParams(&params, r)
if err != nil {
wErr(w, r, badRequest(err))
return
}
inc, err := incs.Incident(ctx, params.ID)
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, inc)
}
func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
urlParams := struct {
ID uuid.UUID `param:"id"`
}{}
err := decodeParams(&urlParams, r)
if err != nil {
wErr(w, r, badRequest(err))
return
}
p := incstore.UpdateIncidentParams{}
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
if err != nil {
wErr(w, r, badRequest(err))
return
}
inc, err := incs.UpdateIncident(ctx, urlParams.ID, p)
if err != nil {
wErr(w, r, autoError(err))
return
}
respond(w, r, inc)
}
func (ia *incidentsAPI) deleteIncident(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
urlParams := struct {
ID uuid.UUID `param:"id"`
}{}
err := decodeParams(&urlParams, r)
if err != nil {
wErr(w, r, badRequest(err))
return
}
err = incs.DeleteIncident(ctx, urlParams.ID)
if err != nil {
wErr(w, r, autoError(err))
return
}
w.WriteHeader(http.StatusNoContent)
}

View file

@ -8,9 +8,11 @@ import (
"dynatron.me/x/stillbox/pkg/alerting"
"dynatron.me/x/stillbox/pkg/auth"
"dynatron.me/x/stillbox/pkg/calls/callstore"
"dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/database/partman"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"dynatron.me/x/stillbox/pkg/nexus"
"dynatron.me/x/stillbox/pkg/notify"
"dynatron.me/x/stillbox/pkg/rest"
@ -44,6 +46,8 @@ type Server struct {
rest rest.API
partman partman.PartitionManager
users users.Store
calls callstore.Store
incidents incstore.Store
}
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
@ -82,6 +86,8 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
sinks: sinks.NewSinkManager(),
rest: api,
users: users.NewStore(),
calls: callstore.NewStore(),
incidents: incstore.NewStore(),
}
if cfg.DB.Partition.Enabled {
@ -129,14 +135,22 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
return srv, nil
}
func (s *Server) addStoresTo(ctx context.Context) context.Context {
ctx = database.CtxWithDB(ctx, s.db)
ctx = tgstore.CtxWithStore(ctx, s.tgs)
ctx = users.CtxWithStore(ctx, s.users)
ctx = callstore.CtxWithStore(ctx, s.calls)
ctx = incstore.CtxWithStore(ctx, s.incidents)
return ctx
}
func (s *Server) Go(ctx context.Context) error {
defer database.Close(s.db)
s.installHupHandler()
ctx = database.CtxWithDB(ctx, s.db)
ctx = tgstore.CtxWithStore(ctx, s.tgs)
ctx = users.CtxWithStore(ctx, s.users)
ctx = s.addStoresTo(ctx)
httpSrv := &http.Server{
Addr: s.conf.Listen,

50
pkg/store/store.go Normal file
View file

@ -0,0 +1,50 @@
package store
import (
"context"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"dynatron.me/x/stillbox/pkg/users"
)
type Store interface {
TG() tgstore.Store
User() users.Store
}
type store struct {
tg tgstore.Store
user users.Store
}
func (s *store) TG() tgstore.Store {
return s.tg
}
func (s *store) User() users.Store {
return s.user
}
func New() Store {
return &store{
tg: tgstore.NewCache(),
user: users.NewStore(),
}
}
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 New()
}
return s
}

View file

@ -0,0 +1,110 @@
-- name: AddToIncident :exec
WITH inp AS (
SELECT
UNNEST(@call_ids::UUID[]) id,
UNNEST(@notes::JSONB[]) notes
) INSERT INTO incidents_calls(
incident_id,
call_id,
calls_tbl_id,
call_date,
notes
)
SELECT
@incident_id::UUID,
inp.id,
inp.id,
c.call_date,
inp.notes
FROM inp
JOIN calls c ON c.id = inp.id
;
-- name: CreateIncident :one
INSERT INTO incidents (
id,
name,
description,
start_time,
end_time,
location,
metadata
) VALUES (
@id,
@name,
sqlc.narg('description'),
sqlc.narg('start_time'),
sqlc.narg('end_time'),
sqlc.narg('location'),
sqlc.narg('metadata')
)
RETURNING *;
-- name: ListIncidentsP :many
SELECT
i.id,
i.name,
i.description,
i.start_time,
i.end_time,
i.location,
i.metadata
FROM incidents i
WHERE
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
i.start_time >= sqlc.narg('start') ELSE TRUE END AND
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
i.start_time <= sqlc.narg('end') ELSE TRUE END
ORDER BY
CASE WHEN @direction::TEXT = 'asc' THEN i.start_time END ASC,
CASE WHEN @direction::TEXT = 'desc' THEN i.start_time END DESC
OFFSET sqlc.arg('offset') ROWS
FETCH NEXT sqlc.arg('per_page') ROWS ONLY
;
-- name: ListIncidentsCount :one
SELECT COUNT(*)
FROM incidents i
WHERE
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
i.start_time >= sqlc.narg('start') ELSE TRUE END AND
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
i.start_time <= sqlc.narg('end') ELSE TRUE END
;
-- name: IncidentCalls :many
-- INCOMPLETE
SELECT
ic.incident_id, call_date,
ic.call_id,
ic.notes
FROM incidents_calls ic;
-- name: GetIncident :one
SELECT
i.id,
i.name,
i.description,
i.start_time,
i.end_time,
i.location,
i.metadata
FROM incidents i
WHERE i.id = @id;
-- name: UpdateIncident :one
UPDATE incidents
SET
name = COALESCE(sqlc.narg('name'), name),
description = COALESCE(sqlc.narg('description'), description),
start_time = COALESCE(sqlc.narg('start_time'), start_time),
end_time = COALESCE(sqlc.narg('end_time'), end_time),
location = COALESCE(sqlc.narg('location'), location),
metadata = COALESCE(sqlc.narg('metadata'), metadata)
WHERE
id = @id
RETURNING *;
-- name: DeleteIncident :exec
DELETE FROM incidents CASCADE WHERE id = @id;

View file

@ -37,6 +37,11 @@ sql:
import: "dynatron.me/x/stillbox/internal/jsontypes"
type: "Metadata"
nullable: true
- column: "incidents.metadata"
go_type:
import: "dynatron.me/x/stillbox/internal/jsontypes"
type: "Metadata"
nullable: true
- column: "pg_catalog.pg_tables.tablename"
go_type: string
nullable: false