diff --git a/internal/jsontypes/location.go b/internal/jsontypes/location.go new file mode 100644 index 0000000..cbd506c --- /dev/null +++ b/internal/jsontypes/location.go @@ -0,0 +1,9 @@ +package jsontypes + +import ( + "encoding/json" +) + +type Location struct { + json.RawMessage +} diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 5a8543f..257c18e 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -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 diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go new file mode 100644 index 0000000..0091d1f --- /dev/null +++ b/pkg/database/incidents.sql.go @@ -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 +} diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index 07111c4..45cf1b7 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -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) diff --git a/pkg/database/models.go b/pkg/database/models.go index 6a6f56b..2448e92 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -56,13 +56,13 @@ type Call struct { } 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"` - Location []byte `json:"location,omitempty"` - Metadata []byte `json:"metadata,omitempty"` + ID uuid.UUID `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + StartTime pgtype.Timestamptz `json:"start_time,omitempty"` + EndTime pgtype.Timestamptz `json:"end_time,omitempty"` + Location []byte `json:"location,omitempty"` + Metadata jsontypes.Metadata `json:"metadata,omitempty"` } type IncidentsCall struct { diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 3606fb1..8997dce 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -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 diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go new file mode 100644 index 0000000..8f18e89 --- /dev/null +++ b/pkg/incidents/incident.go @@ -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"` +} diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go new file mode 100644 index 0000000..933d131 --- /dev/null +++ b/pkg/incidents/incstore/store.go @@ -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) +} diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 2532ef4..11e5e05 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -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 } diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go new file mode 100644 index 0000000..564d282 --- /dev/null +++ b/pkg/rest/incidents.go @@ -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(¶ms, 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) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index a6e7675..729b648 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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" @@ -28,22 +30,24 @@ import ( const shutdownTimeout = 5 * time.Second type Server struct { - auth *auth.Auth - conf *config.Configuration - db database.Store - r *chi.Mux - sources sources.Sources - sinks sinks.Sinks - relayer *sinks.RelayManager - nex *nexus.Nexus - logger *Logger - alerter alerting.Alerter - notifier notify.Notifier - hup chan os.Signal - tgs tgstore.Store - rest rest.API - partman partman.PartitionManager - users users.Store + auth *auth.Auth + conf *config.Configuration + db database.Store + r *chi.Mux + sources sources.Sources + sinks sinks.Sinks + relayer *sinks.RelayManager + nex *nexus.Nexus + logger *Logger + alerter alerting.Alerter + notifier notify.Notifier + hup chan os.Signal + tgs tgstore.Store + 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) { @@ -70,18 +74,20 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { api := rest.New() srv := &Server{ - auth: authenticator, - conf: cfg, - db: db, - r: r, - nex: nexus.New(), - logger: logger, - alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)), - notifier: notifier, - tgs: tgCache, - sinks: sinks.NewSinkManager(), - rest: api, - users: users.NewStore(), + auth: authenticator, + conf: cfg, + db: db, + r: r, + nex: nexus.New(), + logger: logger, + alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)), + notifier: notifier, + tgs: tgCache, + 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, diff --git a/pkg/store/store.go b/pkg/store/store.go new file mode 100644 index 0000000..bb85c85 --- /dev/null +++ b/pkg/store/store.go @@ -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 +} diff --git a/sql/postgres/queries/incidents.sql b/sql/postgres/queries/incidents.sql new file mode 100644 index 0000000..ee7ed74 --- /dev/null +++ b/sql/postgres/queries/incidents.sql @@ -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; diff --git a/sql/sqlc.yaml b/sql/sqlc.yaml index 0b51630..a5c0059 100644 --- a/sql/sqlc.yaml +++ b/sql/sqlc.yaml @@ -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