From b0aab14b8c0d918684d29077798b5ae3097e3862 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 12 Jan 2025 19:40:36 -0500 Subject: [PATCH] Share initial WIP --- go.mod | 1 + go.sum | 2 + pkg/database/mocks/Store.go | 197 +++++++++++++++++++++ pkg/database/models.go | 8 + pkg/database/querier.go | 4 + pkg/database/share.sql.go | 84 +++++++++ pkg/rbac/rbac.go | 1 + pkg/rest/api.go | 1 + pkg/rest/share.go | 39 ++++ pkg/server/server.go | 5 + pkg/share/service.go | 49 +++++ pkg/share/share.go | 61 +++++++ pkg/share/store.go | 85 +++++++++ sql/postgres/migrations/001_initial.up.sql | 8 + sql/postgres/queries/share.sql | 24 +++ 15 files changed, 569 insertions(+) create mode 100644 pkg/database/share.sql.go create mode 100644 pkg/rbac/rbac.go create mode 100644 pkg/rest/share.go create mode 100644 pkg/share/service.go create mode 100644 pkg/share/share.go create mode 100644 pkg/share/store.go create mode 100644 sql/postgres/queries/share.sql diff --git a/go.mod b/go.mod index 5429915..29b60a3 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/matoous/go-nanoid v1.5.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 1efbc87..9228be0 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4= +github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index b308e41..86f8601 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -502,6 +502,53 @@ func (_c *Store_CreatePartition_Call) RunAndReturn(run func(context.Context, str return _c } +// CreateShare provides a mock function with given fields: ctx, arg +func (_m *Store) CreateShare(ctx context.Context, arg database.CreateShareParams) error { + ret := _m.Called(ctx, arg) + + if len(ret) == 0 { + panic("no return value specified for CreateShare") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, database.CreateShareParams) error); ok { + r0 = rf(ctx, arg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Store_CreateShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateShare' +type Store_CreateShare_Call struct { + *mock.Call +} + +// CreateShare is a helper method to define mock.On call +// - ctx context.Context +// - arg database.CreateShareParams +func (_e *Store_Expecter) CreateShare(ctx interface{}, arg interface{}) *Store_CreateShare_Call { + return &Store_CreateShare_Call{Call: _e.mock.On("CreateShare", ctx, arg)} +} + +func (_c *Store_CreateShare_Call) Run(run func(ctx context.Context, arg database.CreateShareParams)) *Store_CreateShare_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(database.CreateShareParams)) + }) + return _c +} + +func (_c *Store_CreateShare_Call) Return(_a0 error) *Store_CreateShare_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Store_CreateShare_Call) RunAndReturn(run func(context.Context, database.CreateShareParams) error) *Store_CreateShare_Call { + _c.Call.Return(run) + return _c +} + // CreateSystem provides a mock function with given fields: ctx, iD, name func (_m *Store) CreateSystem(ctx context.Context, iD int, name string) error { ret := _m.Called(ctx, iD, name) @@ -795,6 +842,53 @@ func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid return _c } +// DeleteShare provides a mock function with given fields: ctx, id +func (_m *Store) DeleteShare(ctx context.Context, id string) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteShare") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Store_DeleteShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteShare' +type Store_DeleteShare_Call struct { + *mock.Call +} + +// DeleteShare is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *Store_Expecter) DeleteShare(ctx interface{}, id interface{}) *Store_DeleteShare_Call { + return &Store_DeleteShare_Call{Call: _e.mock.On("DeleteShare", ctx, id)} +} + +func (_c *Store_DeleteShare_Call) Run(run func(ctx context.Context, id string)) *Store_DeleteShare_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Store_DeleteShare_Call) Return(_a0 error) *Store_DeleteShare_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Store_DeleteShare_Call) RunAndReturn(run func(context.Context, string) error) *Store_DeleteShare_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) @@ -1436,6 +1530,63 @@ func (_c *Store_GetIncidentCalls_Call) RunAndReturn(run func(context.Context, uu return _c } +// GetShare provides a mock function with given fields: ctx, id +func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetShare") + } + + var r0 database.Share + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (database.Share, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) database.Share); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(database.Share) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_GetShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetShare' +type Store_GetShare_Call struct { + *mock.Call +} + +// GetShare is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *Store_Expecter) GetShare(ctx interface{}, id interface{}) *Store_GetShare_Call { + return &Store_GetShare_Call{Call: _e.mock.On("GetShare", ctx, id)} +} + +func (_c *Store_GetShare_Call) Run(run func(ctx context.Context, id string)) *Store_GetShare_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Store_GetShare_Call) Return(_a0 database.Share, _a1 error) *Store_GetShare_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (database.Share, error)) *Store_GetShare_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) @@ -2887,6 +3038,52 @@ func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, data return _c } +// PruneShares provides a mock function with given fields: ctx +func (_m *Store) PruneShares(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for PruneShares") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Store_PruneShares_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneShares' +type Store_PruneShares_Call struct { + *mock.Call +} + +// PruneShares is a helper method to define mock.On call +// - ctx context.Context +func (_e *Store_Expecter) PruneShares(ctx interface{}) *Store_PruneShares_Call { + return &Store_PruneShares_Call{Call: _e.mock.On("PruneShares", ctx)} +} + +func (_c *Store_PruneShares_Call) Run(run func(ctx context.Context)) *Store_PruneShares_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Store_PruneShares_Call) Return(_a0 error) *Store_PruneShares_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Store_PruneShares_Call) RunAndReturn(run func(context.Context) error) *Store_PruneShares_Call { + _c.Call.Return(run) + return _c +} + // RemoveFromIncident provides a mock function with given fields: ctx, iD, callIds func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error { ret := _m.Called(ctx, iD, callIds) diff --git a/pkg/database/models.go b/pkg/database/models.go index 66ff0ed..c1e75da 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -80,6 +80,14 @@ type Setting struct { Value []byte `json:"value,omitempty"` } +type Share struct { + ID string `json:"id,omitempty"` + EntityType string `json:"entity_type,omitempty"` + EntityID uuid.UUID `json:"entity_id,omitempty"` + Owner int `json:"owner,omitempty"` + Expiration pgtype.Timestamptz `json:"expiration,omitempty"` +} + type SweptCall struct { ID uuid.UUID `json:"id,omitempty"` Submitter *int32 `json:"submitter,omitempty"` diff --git a/pkg/database/querier.go b/pkg/database/querier.go index 4ef8f5e..eac7252 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -19,10 +19,12 @@ type Querier interface { 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) + CreateShare(ctx context.Context, arg CreateShareParams) 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 + DeleteShare(ctx context.Context, id string) error DeleteSystem(ctx context.Context, id int) error DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error DeleteUser(ctx context.Context, username string) error @@ -33,6 +35,7 @@ type Querier interface { GetDatabaseSize(ctx context.Context) (string, error) GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) + GetShare(ctx context.Context, id string) (Share, 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) @@ -54,6 +57,7 @@ type Querier interface { ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error) + PruneShares(ctx context.Context) error RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error) SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error diff --git a/pkg/database/share.sql.go b/pkg/database/share.sql.go new file mode 100644 index 0000000..b7b76a3 --- /dev/null +++ b/pkg/database/share.sql.go @@ -0,0 +1,84 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: share.sql + +package database + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createShare = `-- name: CreateShare :exec +INSERT INTO shares ( + id, + entity_type, + entity_id, + owner, + expiration +) VALUES ($1, $2, $3, $4, $5) +` + +type CreateShareParams struct { + ID string `json:"id"` + EntityType string `json:"entity_type"` + EntityID uuid.UUID `json:"entity_id"` + Owner int `json:"owner"` + Expiration pgtype.Timestamptz `json:"expiration"` +} + +func (q *Queries) CreateShare(ctx context.Context, arg CreateShareParams) error { + _, err := q.db.Exec(ctx, createShare, + arg.ID, + arg.EntityType, + arg.EntityID, + arg.Owner, + arg.Expiration, + ) + return err +} + +const deleteShare = `-- name: DeleteShare :exec +DELETE FROM shares WHERE id = $1 +` + +func (q *Queries) DeleteShare(ctx context.Context, id string) error { + _, err := q.db.Exec(ctx, deleteShare, id) + return err +} + +const getShare = `-- name: GetShare :one +SELECT + id, + entity_type, + entity_id, + owner, + expiration +FROM shares +WHERE id = $1 +` + +func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) { + row := q.db.QueryRow(ctx, getShare, id) + var i Share + err := row.Scan( + &i.ID, + &i.EntityType, + &i.EntityID, + &i.Owner, + &i.Expiration, + ) + return i, err +} + +const pruneShares = `-- name: PruneShares :exec +DELETE FROM shares WHERE expiration < NOW() +` + +func (q *Queries) PruneShares(ctx context.Context) error { + _, err := q.db.Exec(ctx, pruneShares) + return err +} diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go new file mode 100644 index 0000000..0b31261 --- /dev/null +++ b/pkg/rbac/rbac.go @@ -0,0 +1 @@ +package rbac diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 50bed21..70c2881 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -37,6 +37,7 @@ func (a *api) Subrouter() http.Handler { r.Mount("/call", new(callsAPI).Subrouter()) r.Mount("/user", new(usersAPI).Subrouter()) r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter()) + r.Mount("/share", newShareHandler(&a.baseURL).Subrouter()) return r } diff --git a/pkg/rest/share.go b/pkg/rest/share.go new file mode 100644 index 0000000..2082883 --- /dev/null +++ b/pkg/rest/share.go @@ -0,0 +1,39 @@ +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "dynatron.me/x/stillbox/internal/common" + "dynatron.me/x/stillbox/internal/forms" + "dynatron.me/x/stillbox/internal/jsontypes" + "dynatron.me/x/stillbox/pkg/incidents" + "dynatron.me/x/stillbox/pkg/incidents/incstore" + "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" +) + +type shareAPI struct { + baseURL *url.URL +} + +func newShareHandler(baseURL *url.URL) API { + return &shareAPI{baseURL} +} + +func (ia *shareAPI) Subrouter() http.Handler { + r := chi.NewMux() + + //r.Get(`/{id:[A-Za-z0-9_-]{20,}}`, ia.getShare) + //r.Post('/create', ia.createShare) + //r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, ia.deleteShare) + //r.Get(`/`, ia.getShares) + + return r +} + diff --git a/pkg/server/server.go b/pkg/server/server.go index 0ea300d..963151e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -16,6 +16,7 @@ import ( "dynatron.me/x/stillbox/pkg/nexus" "dynatron.me/x/stillbox/pkg/notify" "dynatron.me/x/stillbox/pkg/rest" + "dynatron.me/x/stillbox/pkg/share" "dynatron.me/x/stillbox/pkg/sinks" "dynatron.me/x/stillbox/pkg/sources" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" @@ -48,6 +49,7 @@ type Server struct { users users.Store calls callstore.Store incidents incstore.Store + share share.Service } func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { @@ -85,6 +87,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { tgs: tgCache, sinks: sinks.NewSinkManager(), rest: api, + share: share.NewService(), users: users.NewStore(), calls: callstore.NewStore(), incidents: incstore.NewStore(), @@ -141,6 +144,7 @@ func (s *Server) addStoresTo(ctx context.Context) context.Context { ctx = users.CtxWithStore(ctx, s.users) ctx = callstore.CtxWithStore(ctx, s.calls) ctx = incstore.CtxWithStore(ctx, s.incidents) + ctx = share.CtxWithStore(ctx, s.share.Store()) return ctx } @@ -159,6 +163,7 @@ func (s *Server) Go(ctx context.Context) error { go s.nex.Go(ctx) go s.alerter.Go(ctx) + go s.share.Go(ctx) if pm := s.partman; pm != nil { go pm.Go(ctx) diff --git a/pkg/share/service.go b/pkg/share/service.go new file mode 100644 index 0000000..8a08f0d --- /dev/null +++ b/pkg/share/service.go @@ -0,0 +1,49 @@ +package share + +import ( + "context" + "time" + + "github.com/rs/zerolog/log" +) + +const ( + PruneInterval = time.Hour * 4 +) + +type Service interface { + Store() Store + + Go(ctx context.Context) +} + +type service struct { + store Store +} + +func (s *service) Store() Store { + return s.store +} + +func (s *service) Go(ctx context.Context) { + tick := time.NewTicker(PruneInterval) + + for { + select { + case <- tick.C: + err := s.store.Prune(ctx) + if err != nil { + log.Error().Err(err).Msg("share prune failed") + } + case <-ctx.Done(): + tick.Stop() + return + } + } +} + +func NewService() *service { + return &service{ + store: NewStore(), + } +} diff --git a/pkg/share/share.go b/pkg/share/share.go new file mode 100644 index 0000000..204dfc9 --- /dev/null +++ b/pkg/share/share.go @@ -0,0 +1,61 @@ +package share + +import ( + "context" + "time" + + "dynatron.me/x/stillbox/internal/jsontypes" + + "github.com/google/uuid" + "github.com/matoous/go-nanoid" +) + +const ( + SlugLength = 20 +) + +type EntityType string + +const ( + EntityIncident EntityType = "incident" + EntityCall EntityType = "call" +) + +// If an incident is shared, all calls that are part of it must be shared too, but this can be through the incident share (/share/bLaH/callID[.mp3]) + +type Share struct { + ID string `json:"id"` + Type EntityType `json:"entityType"` + EntityID uuid.UUID `json:"entityID"` + Expiration *jsontypes.Time `json:"expiration"` +} + +// NewShare creates a new share. +func NewShare(ctx context.Context, shType EntityType, shID uuid.UUID, exp *time.Duration) (id string, err error) { + id, err = gonanoid.ID(SlugLength) + if err != nil { + return + } + + store := FromCtx(ctx) + + var expT *jsontypes.Time + if exp != nil { + tt := time.Now().Add(*exp) + expT = (*jsontypes.Time)(&tt) + } + + share := &Share{ + ID: id, + Type: shType, + EntityID: shID, + Expiration: expT, + } + + err = store.Create(ctx, share) + if err != nil { + return + } + + return id, nil +} diff --git a/pkg/share/store.go b/pkg/share/store.go new file mode 100644 index 0000000..8c75d66 --- /dev/null +++ b/pkg/share/store.go @@ -0,0 +1,85 @@ +package share + +import ( + "context" + + "dynatron.me/x/stillbox/internal/jsontypes" + "dynatron.me/x/stillbox/pkg/database" +) + +type Store interface { + // Get retreives a share record. + Get(ctx context.Context, id string) (*Share, error) + + // Create stores a new share record. + Create(ctx context.Context, share *Share) error + + // Delete deletes a share record. + Delete(ctx context.Context, id string) error + + // Prune removes expired share records. + Prune(ctx context.Context) error +} + +type postgresStore struct { +} + +func recToShare(share database.Share) *Share { + return &Share{ + ID: share.ID, + Type: EntityType(share.EntityType), + EntityID: share.EntityID, + Expiration: jsontypes.TimePtrFromTSTZ(share.Expiration), + } +} + +func (s *postgresStore) Get(ctx context.Context, id string) (*Share, error) { + db := database.FromCtx(ctx) + rec, err := db.GetShare(ctx, id) + if err != nil { + return nil, err + } + + return recToShare(rec), nil +} + +func (s *postgresStore) Create(ctx context.Context, share *Share) error { + db := database.FromCtx(ctx) + err := db.CreateShare(ctx, database.CreateShareParams{ + ID: share.ID, + EntityType: string(share.Type), + EntityID: share.EntityID, + Expiration: share.Expiration.PGTypeTSTZ(), + }) + + return err +} + +func (s *postgresStore) Delete(ctx context.Context, id string) error { + return database.FromCtx(ctx).DeleteShare(ctx, id) +} + +func (s *postgresStore) Prune(ctx context.Context) error { + return database.FromCtx(ctx).PruneShares(ctx) +} + +func NewStore() *postgresStore { + return new(postgresStore) +} + +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 +} diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql index b9c9304..6657f60 100644 --- a/sql/postgres/migrations/001_initial.up.sql +++ b/sql/postgres/migrations/001_initial.up.sql @@ -163,3 +163,11 @@ CREATE TABLE IF NOT EXISTS incidents_calls( FOREIGN KEY (calls_tbl_id, call_date) REFERENCES calls(id, call_date), PRIMARY KEY (incident_id, call_id) ); + +CREATE TABLE IF NOT EXISTS shares( + id TEXT PRIMARY KEY, + entity_type TEXT NOT NULL, + entity_id UUID NOT NULL, + owner INTEGER NOT NULL REFERENCES users(id), + expiration TIMESTAMPTZ NULL +); diff --git a/sql/postgres/queries/share.sql b/sql/postgres/queries/share.sql new file mode 100644 index 0000000..11931e3 --- /dev/null +++ b/sql/postgres/queries/share.sql @@ -0,0 +1,24 @@ +-- name: GetShare :one +SELECT + id, + entity_type, + entity_id, + owner, + expiration +FROM shares +WHERE id = @id; + +-- name: CreateShare :exec +INSERT INTO shares ( + id, + entity_type, + entity_id, + owner, + expiration +) VALUES (@id, @entity_type, @entity_id, @owner, sqlc.narg('expiration')); + +-- name: DeleteShare :exec +DELETE FROM shares WHERE id = @id; + +-- name: PruneShares :exec +DELETE FROM shares WHERE expiration < NOW();