From a656d45a6d097f4f616d31ea04d87c8368ab3614 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 1 Apr 2026 18:12:46 +0000 Subject: [PATCH] Move QueryParams to resource module --- api/communication.go | 66 ------------ api/handler.go | 5 +- api/lead.go | 50 --------- api/resource.go | 14 --- api/review_task.go | 152 --------------------------- api/routes.go | 39 ++++--- api/signal.go | 27 ----- api/upload.go | 140 ------------------------- api/user.go | 114 --------------------- resource/communication.go | 77 ++++++++++++++ resource/lead.go | 62 +++++++++++ {api => resource}/query_params.go | 10 +- resource/review_task.go | 164 ++++++++++++++++++++++++++++++ resource/signal.go | 39 +++++++ resource/upload.go | 130 +++++++++++++++++++++++ resource/user.go | 135 ++++++++++++++++++++++++ 16 files changed, 641 insertions(+), 583 deletions(-) create mode 100644 resource/communication.go create mode 100644 resource/lead.go rename {api => resource}/query_params.go (72%) create mode 100644 resource/review_task.go create mode 100644 resource/signal.go create mode 100644 resource/upload.go create mode 100644 resource/user.go diff --git a/api/communication.go b/api/communication.go index 56f6edd6..778f64ec 100644 --- a/api/communication.go +++ b/api/communication.go @@ -1,67 +1 @@ package api - -import ( - "context" - "net/http" - "slices" - "time" - - "github.com/Gleipnir-Technology/nidus-sync/config" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/Gleipnir-Technology/nidus-sync/platform/publicreport" - "github.com/Gleipnir-Technology/nidus-sync/platform/types" - "github.com/google/uuid" - //"github.com/rs/zerolog/log" -) - -type communication struct { - Created time.Time `json:"created"` - ID string `json:"id"` - PublicReport types.PublicReport `json:"public_report"` - Type string `json:"type"` -} -type contentListCommunication struct { - Communications []communication `json:"communications"` -} - -func listCommunication(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListCommunication, *nhttp.ErrorWithStatus) { - reports, err := publicreport.ReportsForOrganization(ctx, user.Organization.ID) - if err != nil { - return nil, nhttp.NewError("nuisance report query: %w", err) - } - comms := make([]communication, len(reports)) - for i, report := range reports { - comms[i] = communication{ - Created: report.Created, - ID: report.PublicID, - PublicReport: report, - Type: "publicreport." + string(report.Type), - } - } - _by_created := func(a, b communication) int { - if a.Created == b.Created { - return 0 - } else if a.Created.Before(b.Created) { - return 1 - } else { - return -1 - } - } - slices.SortFunc(comms, _by_created) - return &contentListCommunication{ - Communications: comms, - }, nil -} - -func toImageURLs(m map[string][]uuid.UUID, id string) []string { - uuids, ok := m[id] - if !ok { - return []string{} - } - urls := make([]string, len(uuids)) - for i, u := range uuids { - urls[i] = config.MakeURLNidus("/api/image/%s/content", u.String()) - } - return urls -} diff --git a/api/handler.go b/api/handler.go index a6d488ad..fb6c899a 100644 --- a/api/handler.go +++ b/api/handler.go @@ -12,13 +12,14 @@ import ( nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/platform/file" + "github.com/Gleipnir-Technology/nidus-sync/resource" "github.com/gorilla/schema" "github.com/rs/zerolog/log" ) var decoder = schema.NewDecoder() -type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, queryParams) (*T, *nhttp.ErrorWithStatus) +type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus) type wrappedHandler func(http.ResponseWriter, *http.Request) type contentAuthenticated[T any] struct { C T @@ -34,7 +35,7 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler { return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) { ctx := r.Context() var body []byte - var params queryParams + var params resource.QueryParams err := decoder.Decode(¶ms, r.URL.Query()) if err != nil { log.Error().Err(err).Msg("decode query failure") diff --git a/api/lead.go b/api/lead.go index 25b04f9a..778f64ec 100644 --- a/api/lead.go +++ b/api/lead.go @@ -1,51 +1 @@ package api - -import ( - "context" - "fmt" - "net/http" - - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" -) - -type createLead struct { - PoolLocations map[int]platform.Location `json:"pool_locations"` - SignalIDs []int `json:"signal_ids"` -} -type contentListLead struct { - Leads []lead `json:"leads"` -} -type lead struct { - ID int32 `json:"id"` -} - -func listLead(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListLead, *nhttp.ErrorWithStatus) { - return &contentListLead{ - Leads: make([]lead, 0), - }, nil -} -func postLeads(ctx context.Context, r *http.Request, user platform.User, req createLead) (string, *nhttp.ErrorWithStatus) { - if len(req.SignalIDs) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals") - } - if len(req.SignalIDs) > 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") - } - signal_id := req.SignalIDs[0] - var pool_location *platform.Location - l, ok := req.PoolLocations[signal_id] - if ok { - pool_location = &l - } - site_id, err := platform.SiteFromSignal(ctx, user, int32(signal_id)) - if err != nil || site_id == nil { - return "", nhttp.NewError("site from signal: %w", err) - } - lead_id, err := platform.LeadCreate(ctx, user, int32(signal_id), *site_id, pool_location) - if err != nil || lead_id == nil { - return "", nhttp.NewError("lead create: %w", err) - } - - return fmt.Sprintf("/lead/%d", *lead_id), nil -} diff --git a/api/resource.go b/api/resource.go index 830deb56..778f64ec 100644 --- a/api/resource.go +++ b/api/resource.go @@ -1,15 +1 @@ package api - -import ( -//"encoding/json" -) - -type resource[T any] struct { - inner *T -} - -func newResource[T any](inner *T) resource[T] { - return resource[T]{ - inner: inner, - } -} diff --git a/api/review_task.go b/api/review_task.go index 3debaa52..778f64ec 100644 --- a/api/review_task.go +++ b/api/review_task.go @@ -1,153 +1 @@ package api - -import ( - "context" - "net/http" - "time" - - "github.com/Gleipnir-Technology/bob" - "github.com/Gleipnir-Technology/bob/dialect/psql" - "github.com/Gleipnir-Technology/bob/dialect/psql/sm" - "github.com/Gleipnir-Technology/nidus-sync/db" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/Gleipnir-Technology/nidus-sync/platform/types" - //"github.com/aarondl/opt/null" - "github.com/stephenafamo/scan" -) - -type reviewTask struct { - Address types.Address `json:"address"` - Created time.Time `json:"created"` - Creator platform.User `json:"creator"` - ID int32 `json:"id"` - Location types.Location `json:"location"` - Pool reviewTaskPool `json:"pool"` - Reviewed *time.Time `json:"addressed"` - Reviewer *platform.User `json:"addressor"` -} -type reviewTaskPool struct { - Condition string `json:"condition"` - Site types.Site `json:"site"` -} -type contentListReviewTask struct { - Tasks []reviewTask `json:"tasks"` - Total int32 `json:"total"` -} - -func listReviewTask(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListReviewTask, *nhttp.ErrorWithStatus) { - limit := 20 - if query.Limit != nil { - limit = *query.Limit - } - type _RowTotal struct { - Total int32 `db:"total"` - } - row_total, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "COUNT(*) AS total", - ), - sm.From("review_task"), - sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID))), - sm.Where(psql.Quote("review_task", "reviewed").IsNull()), - ), scan.StructMapper[_RowTotal]()) - if err != nil { - return nil, nhttp.NewError("failed to total count: %w", err) - } - - type _Row struct { - Address types.Address `db:"address"` - Condition string `db:"condition"` - Created time.Time `db:"created"` - CreatorID int32 `db:"creator_id"` - ID int32 `db:"id"` - Latitude float64 `db:"latitude"` - Longitude float64 `db:"longitude"` - Reviewed *time.Time `db:"reviewed"` - ReviewerID *int32 `db:"reviewer_id"` - Species *string `db:"species"` - Title string `db:"title"` - Type string `db:"type"` - } - rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "feature_pool.condition AS condition", - "review_task.created AS created", - "review_task.creator_id AS creator_id", - "review_task.id AS id", - "review_task.reviewed AS reviewed", - "review_task.reviewer_id AS reviewer_id", - "address.country AS \"address.country\"", - "address.locality AS \"address.locality\"", - "address.number_ AS \"address.number\"", - "address.postal_code AS \"address.postal_code\"", - "address.region AS \"address.region\"", - "address.street AS \"address.street\"", - "address.unit AS \"address.unit\"", - "ST_Y(address.location) AS latitude", - "ST_X(address.location) AS longitude", - ), - sm.From("review_task_pool"), - sm.InnerJoin("feature_pool").OnEQ( - psql.Quote("review_task_pool", "feature_pool_id"), - psql.Quote("feature_pool", "feature_id"), - ), - sm.InnerJoin("review_task").OnEQ( - psql.Quote("review_task_pool", "review_task_id"), - psql.Quote("review_task", "id"), - ), - sm.InnerJoin("feature").OnEQ( - psql.Quote("feature_pool", "feature_id"), - psql.Quote("feature", "id"), - ), - sm.InnerJoin("site").On( - psql.Quote("feature", "site_id").EQ(psql.Quote("site", "id")), - ), - sm.InnerJoin("address").OnEQ( - psql.Quote("site", "address_id"), - psql.Quote("address", "id"), - ), - sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID))), - sm.Where(psql.Quote("review_task", "reviewed").IsNull()), - sm.Limit(limit), - ), scan.StructMapper[_Row]()) - if err != nil { - return nil, nhttp.NewError("failed to get review tasks: %w", err) - } - users_by_id, err := platform.UsersByOrg(ctx, user.Organization) - if err != nil { - return nil, nhttp.NewError("users by id: %w", err) - } - tasks := make([]reviewTask, len(rows)) - for i, row := range rows { - tasks[i] = reviewTask{ - Address: row.Address, - Created: row.Created, - Creator: *users_by_id[row.CreatorID], - ID: row.ID, - Location: types.Location{ - Latitude: row.Latitude, - Longitude: row.Longitude, - }, - Pool: reviewTaskPool{ - Condition: row.Condition, - }, - Reviewed: row.Reviewed, - Reviewer: userOrNil(users_by_id, row.ReviewerID), - } - } - return &contentListReviewTask{ - Tasks: tasks, - Total: row_total.Total, - }, nil -} -func userOrNil(usersByID map[int32]*platform.User, id *int32) *platform.User { - if id == nil { - return nil - } - u, ok := usersByID[*id] - if !ok { - return nil - } - return u -} diff --git a/api/routes.go b/api/routes.go index 58769dfe..8fb699e3 100644 --- a/api/routes.go +++ b/api/routes.go @@ -3,6 +3,7 @@ package api import ( "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/platform/file" + "github.com/Gleipnir-Technology/nidus-sync/resource" "github.com/gorilla/mux" ) @@ -16,37 +17,45 @@ func AddRoutes(r *mux.Router) { r.Handle("/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost)).Methods("POST") r.Handle("/avatar", authenticatedHandlerPostMultipart(avatarPost, file.CollectionAvatar)).Methods("POST") r.Handle("/client/ios", auth.NewEnsureAuth(handleClientIos)).Methods("GET") - r.Handle("/communication", authenticatedHandlerJSON(listCommunication)).Methods("GET") + communication := resource.Communication(r) + r.Handle("/communication", authenticatedHandlerJSON(communication.List)).Methods("GET") r.Handle("/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)).Methods("POST") r.Handle("/events", auth.NewEnsureAuth(streamEvents)).Methods("GET") r.Handle("/image/{uuid}", auth.NewEnsureAuth(apiImagePost)).Methods("POST") r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentGet)).Methods("GET") r.Handle("/image/{uuid}/content", auth.NewEnsureAuth(apiImageContentPost)).Methods("POST") - r.Handle("/leads", authenticatedHandlerJSON(listLead)).Methods("GET") - r.Handle("/leads", authenticatedHandlerJSONPost(postLeads)).Methods("POST") + lead := resource.Lead(r) + r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET") + r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST") r.Handle("/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource)).Methods("GET") r.Handle("/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)).Methods("POST") r.Handle("/publicreport/signal", authenticatedHandlerJSONPost(postPublicreportSignal)).Methods("POST") r.Handle("/publicreport/message", authenticatedHandlerJSONPost(postPublicreportMessage)).Methods("POST") r.Handle("/review/pool", authenticatedHandlerJSONPost(postReviewPool)).Methods("POST") - r.Handle("/review-task", authenticatedHandlerJSON(listReviewTask)).Methods("GET") + review_task := resource.ReviewTask(r) + r.Handle("/review-task", authenticatedHandlerJSON(review_task.List)).Methods("GET") r.Handle("/service-request", auth.NewEnsureAuth(apiServiceRequest)).Methods("GET") - r.Handle("/signal", authenticatedHandlerJSON(listSignal)).Methods("GET") + signal := resource.Signal(r) + r.Handle("/signal", authenticatedHandlerJSON(signal.List)).Methods("GET") r.Handle("/sudo/email", authenticatedHandlerJSONPost(postSudoEmail)).Methods("POST") r.Handle("/sudo/sms", authenticatedHandlerJSONPost(postSudoSMS)).Methods("POST") r.Handle("/sudo/sse", authenticatedHandlerJSONPost(postSudoSSE)).Methods("POST") r.Handle("/trap-data", auth.NewEnsureAuth(apiTrapData)).Methods("GET") r.Handle("/tile/{z}/{y}/{x}", auth.NewEnsureAuth(getTile)).Methods("GET") - r.Handle("/upload/pool/flyover", authenticatedHandlerPostMultipart(postUploadPoolFlyoverCreate, file.CollectionCSV)).Methods("POST") - r.Handle("/upload/pool/custom", authenticatedHandlerPostMultipart(postUploadPoolCustomCreate, file.CollectionCSV)).Methods("POST") - r.Handle("/upload", authenticatedHandlerJSON(getUploadList)).Methods("GET") - r.Handle("/upload/{id}", authenticatedHandlerJSON(getUploadByID)).Methods("GET") - r.Handle("/upload/{id}/commit", authenticatedHandlerJSONPost(postUploadCommit)).Methods("POST") - r.Handle("/upload/{id}/discard", authenticatedHandlerJSONPost(postUploadDiscard)).Methods("POST") - r.Handle("/user/self", authenticatedHandlerJSON(getUserSelf)).Methods("GET") - r.Handle("/user/suggestion", authenticatedHandlerJSON(listUserSuggestion)).Methods("GET") - r.Handle("/user", authenticatedHandlerJSON(listUser)).Methods("GET") - r.Handle("/user/{id}", authenticatedHandlerJSONPut(userPut)).Methods("PUT") + upload := resource.Upload(r) + r.Handle("/upload/pool/flyover", authenticatedHandlerPostMultipart(upload.PoolFlyoverCreate, file.CollectionCSV)).Methods("POST") + r.Handle("/upload/pool/custom", authenticatedHandlerPostMultipart(upload.PoolCustomCreate, file.CollectionCSV)).Methods("POST") + r.Handle("/upload", authenticatedHandlerJSON(upload.List)).Methods("GET") + r.Handle("/upload/{id}", authenticatedHandlerJSON(upload.ByIDGet)).Methods("GET") + r.Handle("/upload/{id}/commit", authenticatedHandlerJSONPost(upload.Commit)).Methods("POST") + r.Handle("/upload/{id}/discard", authenticatedHandlerJSONPost(upload.Discard)).Methods("POST") + + user := resource.NewUser(r) + r.Handle("/user/self", authenticatedHandlerJSON(user.SelfGet)).Methods("GET") + r.Handle("/user/suggestion", authenticatedHandlerJSON(user.SuggestionGet)).Methods("GET") + r.Handle("/user", authenticatedHandlerJSON(user.List)).Methods("GET") + r.Handle("/user/{id}", authenticatedHandlerJSON(user.ByIDGet)).Methods("GET") + r.Handle("/user/{id}", authenticatedHandlerJSONPut(user.ByIDPut)).Methods("PUT") // Unauthenticated endpoints r.HandleFunc("/district", apiGetDistrict).Methods("GET") diff --git a/api/signal.go b/api/signal.go index 8cdc31e1..778f64ec 100644 --- a/api/signal.go +++ b/api/signal.go @@ -1,28 +1 @@ package api - -import ( - "context" - "net/http" - - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - //"github.com/aarondl/opt/null" -) - -type contentListSignal struct { - Signals []*platform.Signal `json:"signals"` -} - -func listSignal(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentListSignal, *nhttp.ErrorWithStatus) { - limit := 20 - if query.Limit != nil { - limit = *query.Limit - } - signals, err := platform.SignalList(ctx, user, limit) - if err != nil { - return nil, nhttp.NewError("list signals: %w", err) - } - return &contentListSignal{ - Signals: signals, - }, nil -} diff --git a/api/upload.go b/api/upload.go index 2e262abe..778f64ec 100644 --- a/api/upload.go +++ b/api/upload.go @@ -1,141 +1 @@ package api - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/Gleipnir-Technology/nidus-sync/html" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/Gleipnir-Technology/nidus-sync/platform/file" - "github.com/gorilla/mux" - "github.com/rs/zerolog/log" -) - -func getUploadByID(ctx context.Context, r *http.Request, u platform.User, query queryParams) (*platform.Upload, *nhttp.ErrorWithStatus) { - vars := mux.Vars(r) - file_id_str := vars["id"] - file_id_, err := strconv.ParseInt(file_id_str, 10, 32) - if err != nil { - return nil, nhttp.NewError("Failed to parse file_id: %w", err) - } - file_id := int32(file_id_) - detail, err := platform.GetUploadDetail(ctx, u.Organization.ID, file_id) - if err != nil { - return nil, nhttp.NewError("Failed to get pool: %w", err) - } - return detail, nil -} - -type contentUploadList struct { - RecentUploads []platform.Upload -} -type contentUploadPlaceholder struct{} - -func getUploadList(ctx context.Context, r *http.Request, user platform.User, req queryParams) (*contentUploadPoolList, *nhttp.ErrorWithStatus) { - rows, err := platform.UploadList(ctx, user.Organization) - if err != nil { - return nil, nhttp.NewError("Get upload list: %w", err) - } - return &contentUploadPoolList{ - Uploads: rows, - }, nil -} - -type contentUploadDetail struct { - CSVFileID int32 - Organization platform.Organization - Upload platform.Upload -} -type contentUploadPoolList struct { - Uploads []platform.Upload `json:"uploads"` -} -type contentUploadPool struct{} - -func getUploadPool(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPool], *nhttp.ErrorWithStatus) { - data := contentUploadPool{} - return html.NewResponse("sync/upload-csv-pool.html", data), nil -} - -type contentUploadPoolFlyoverCreate struct{} - -func getUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolFlyoverCreate], *nhttp.ErrorWithStatus) { - data := contentUploadPoolFlyoverCreate{} - return html.NewResponse("sync/upload-csv-pool-flyover.html", data), nil -} - -type contentUploadPoolCustomCreate struct{} - -func getUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User) (*html.Response[contentUploadPoolCustomCreate], *nhttp.ErrorWithStatus) { - data := contentUploadPoolCustomCreate{} - return html.NewResponse("sync/upload-csv-pool-custom.html", data), nil -} - -type FormUploadCommit struct{} - -func postUploadCommit(ctx context.Context, r *http.Request, u platform.User, f FormUploadCommit) (string, *nhttp.ErrorWithStatus) { - vars := mux.Vars(r) - file_id_str := vars["id"] - file_id_, err := strconv.ParseInt(file_id_str, 10, 32) - if err != nil { - return "", nhttp.NewError("Failed to parse file_id: %w", err) - } - err = platform.UploadCommit(ctx, u.Organization, int32(file_id_), u) - if err != nil { - return "", nhttp.NewError("Failed to mark committed: %w", err) - } - log.Debug().Int64("file_id", file_id_).Int("user_id", u.ID).Msg("Committed file") - return "/configuration/upload", nil -} - -type FormUploadDiscard struct{} - -func postUploadDiscard(ctx context.Context, r *http.Request, u platform.User, f FormUploadDiscard) (string, *nhttp.ErrorWithStatus) { - vars := mux.Vars(r) - file_id_str := vars["id"] - file_id_, err := strconv.ParseInt(file_id_str, 10, 32) - if err != nil { - return "", nhttp.NewError("Failed to parse file_id: %w", err) - } - err = platform.UploadDiscard(ctx, u.Organization, int32(file_id_)) - if err != nil { - return "", nhttp.NewError("Failed to mark discarded: %w", err) - } - return "/configuration/upload", nil -} - -func postUploadPoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { - // If the organization we're uploading to doesn't have a service area, we can't process the upload correctly - if !(u.Organization.HasServiceArea() || u.Organization.IsCatchall()) { - return "", nhttp.NewErrorStatus(http.StatusConflict, "Your organization does not yet have a service area") - } - if len(uploads) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") - } - if len(uploads) != 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") - } - upload := uploads[0] - saved_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypeFlyover) - if err != nil { - return "", nhttp.NewError("Failed to create new pool: %w", err) - } - return fmt.Sprintf("/configuration/upload/%d", *saved_upload), nil -} -func postUploadPoolCustomCreate(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { - if len(uploads) == 0 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") - } - if len(uploads) != 1 { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") - } - upload := uploads[0] - pool_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypePoollist) - if err != nil { - return "", nhttp.NewError("Failed to create new pool: %w", err) - } - return fmt.Sprintf("/configuration/upload/%d", *pool_upload), nil -} diff --git a/api/user.go b/api/user.go index 4b3fc02e..778f64ec 100644 --- a/api/user.go +++ b/api/user.go @@ -1,115 +1 @@ package api - -import ( - "context" - "net/http" - "strconv" - - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/html" - nhttp "github.com/Gleipnir-Technology/nidus-sync/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/gorilla/mux" - "github.com/rs/zerolog/log" -) - -type contentURLAPI struct { - Avatar string `json:"avatar"` - Communication string `json:"communication"` - PublicreportMessage string `json:"publicreport_message"` - ReviewTask string `json:"review_task"` - Signal string `json:"signal"` - Upload string `json:"upload"` - User string `json:"user"` -} -type contentURLs struct { - API contentURLAPI `json:"api"` - Tegola string `json:"tegola"` - Tile string `json:"tile"` -} -type contentUserSelf struct { - Self platform.User `json:"self"` - URLs contentURLs `json:"urls"` -} - -func getUserSelf(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*contentUserSelf, *nhttp.ErrorWithStatus) { - counts, err := platform.NotificationCountsForUser(ctx, user) - if err != nil { - return nil, nhttp.NewError("get notifications: %w", err) - } - org, err := platform.OrganizationByID(ctx, int(user.Organization.ID)) - if err != nil { - return nil, nhttp.NewError("get org: %w", err) - } - user.Organization = *org - user.NotificationCounts = *counts - urls := html.NewContentURL() - return &contentUserSelf{ - Self: user, - URLs: contentURLs{ - API: contentURLAPI{ - Avatar: config.MakeURLNidus("/api/avatar"), - Communication: urls.API.Communication, - PublicreportMessage: urls.API.Publicreport.Message, - ReviewTask: config.MakeURLNidus("/api/review-task"), - Signal: config.MakeURLNidus("/api/signal"), - Upload: config.MakeURLNidus("/api/upload"), - User: config.MakeURLNidus("/api/user"), - }, - Tegola: urls.Tegola, - Tile: config.MakeURLNidus("/api/tile/{z}/{y}/{x}"), - }, - }, nil -} - -type responseListUser struct { - Users []*platform.User `json:"users"` -} - -func listUser(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*responseListUser, *nhttp.ErrorWithStatus) { - users, err := platform.UsersByOrg(ctx, user.Organization) - if err != nil { - return nil, nhttp.NewError("list users: %w", err) - } - results := make([]*platform.User, len(users)) - i := 0 - for _, v := range users { - results[i] = v - i++ - } - return &responseListUser{ - Users: results, - }, nil -} - -type responseListUserSuggestion struct { - Users []*platform.User `json:"users"` -} - -func listUserSuggestion(ctx context.Context, r *http.Request, user platform.User, query queryParams) (*responseListUserSuggestion, *nhttp.ErrorWithStatus) { - if query.Query == nil { - return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "you need to include a query") - } - users, err := platform.UserSuggestion(ctx, user, *query.Query) - if err != nil { - return nil, nhttp.NewError("query suggestions: %w", err) - } - return &responseListUserSuggestion{ - Users: users, - }, nil -} - -func userPut(ctx context.Context, r *http.Request, user platform.User, updates platform.UserChangeRequest) (string, *nhttp.ErrorWithStatus) { - log.Info().Str("avatar", updates.Avatar).Msg("doing updates") - vars := mux.Vars(r) - user_id_str := vars["id"] - user_id, err := strconv.Atoi(user_id_str) - if err != nil { - return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user update: %w", err) - } - err = platform.UserUpdate(ctx, user, user_id, updates) - if err != nil { - return "", nhttp.NewError("user update: %w", err) - } - return "", nil -} diff --git a/resource/communication.go b/resource/communication.go new file mode 100644 index 00000000..40aab0f3 --- /dev/null +++ b/resource/communication.go @@ -0,0 +1,77 @@ +package resource + +import ( + "context" + "net/http" + "slices" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/config" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/publicreport" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + "github.com/google/uuid" + "github.com/gorilla/mux" + //"github.com/rs/zerolog/log" +) + +type communicationR struct { + router *mux.Router +} + +func Communication(r *mux.Router) *communicationR { + return &communicationR{ + router: r, + } +} + +type communication struct { + Created time.Time `json:"created"` + ID string `json:"id"` + PublicReport types.PublicReport `json:"public_report"` + Type string `json:"type"` +} +type communicationList struct { + Communications []communication `json:"communications"` +} + +func toImageURLs(m map[string][]uuid.UUID, id string) []string { + uuids, ok := m[id] + if !ok { + return []string{} + } + urls := make([]string, len(uuids)) + for i, u := range uuids { + urls[i] = config.MakeURLNidus("/api/image/%s/content", u.String()) + } + return urls +} +func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*communicationList, *nhttp.ErrorWithStatus) { + reports, err := publicreport.ReportsForOrganization(ctx, user.Organization.ID) + if err != nil { + return nil, nhttp.NewError("nuisance report query: %w", err) + } + comms := make([]communication, len(reports)) + for i, report := range reports { + comms[i] = communication{ + Created: report.Created, + ID: report.PublicID, + PublicReport: report, + Type: "publicreport." + string(report.Type), + } + } + _by_created := func(a, b communication) int { + if a.Created == b.Created { + return 0 + } else if a.Created.Before(b.Created) { + return 1 + } else { + return -1 + } + } + slices.SortFunc(comms, _by_created) + return &communicationList{ + Communications: comms, + }, nil +} diff --git a/resource/lead.go b/resource/lead.go new file mode 100644 index 00000000..b5572e52 --- /dev/null +++ b/resource/lead.go @@ -0,0 +1,62 @@ +package resource + +import ( + "context" + "fmt" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/gorilla/mux" + "net/http" + //"github.com/rs/zerolog/log" +) + +type leadR struct { + router *mux.Router +} + +func Lead(r *mux.Router) *leadR { + return &leadR{ + router: r, + } +} + +type createLead struct { + PoolLocations map[int]platform.Location `json:"pool_locations"` + SignalIDs []int `json:"signal_ids"` +} +type contentListLead struct { + Leads []lead `json:"leads"` +} +type lead struct { + ID int32 `json:"id"` +} + +func (res *leadR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*contentListLead, *nhttp.ErrorWithStatus) { + return &contentListLead{ + Leads: make([]lead, 0), + }, nil +} +func (res *leadR) Create(ctx context.Context, r *http.Request, user platform.User, req createLead) (string, *nhttp.ErrorWithStatus) { + if len(req.SignalIDs) == 0 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with no signals") + } + if len(req.SignalIDs) > 1 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "can't make a lead with multiple signals yet") + } + signal_id := req.SignalIDs[0] + var pool_location *platform.Location + l, ok := req.PoolLocations[signal_id] + if ok { + pool_location = &l + } + site_id, err := platform.SiteFromSignal(ctx, user, int32(signal_id)) + if err != nil || site_id == nil { + return "", nhttp.NewError("site from signal: %w", err) + } + lead_id, err := platform.LeadCreate(ctx, user, int32(signal_id), *site_id, pool_location) + if err != nil || lead_id == nil { + return "", nhttp.NewError("lead create: %w", err) + } + + return fmt.Sprintf("/lead/%d", *lead_id), nil +} diff --git a/api/query_params.go b/resource/query_params.go similarity index 72% rename from api/query_params.go rename to resource/query_params.go index 84138f45..d6568fb4 100644 --- a/api/query_params.go +++ b/resource/query_params.go @@ -1,13 +1,17 @@ -package api +package resource -type queryParams struct { +import ( +//"github.com/gorilla/schema" +) + +type QueryParams struct { Limit *int `schema:"limit"` Query *string `schema:"query"` Sort *string `schema:"sort"` Type *string `schema:"type"` } -func (qp queryParams) SortOrDefault(default_name string, ascending bool) (string, bool) { +func (qp QueryParams) SortOrDefault(default_name string, ascending bool) (string, bool) { if qp.Sort == nil { return default_name, ascending } diff --git a/resource/review_task.go b/resource/review_task.go new file mode 100644 index 00000000..2115f92c --- /dev/null +++ b/resource/review_task.go @@ -0,0 +1,164 @@ +package resource + +import ( + "context" + "net/http" + "time" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/nidus-sync/db" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + //"github.com/aarondl/opt/null" + "github.com/gorilla/mux" + "github.com/stephenafamo/scan" +) + +type reviewTaskR struct { + router *mux.Router +} + +func ReviewTask(r *mux.Router) *reviewTaskR { + return &reviewTaskR{ + router: r, + } +} + +type reviewTask struct { + Address types.Address `json:"address"` + Created time.Time `json:"created"` + Creator platform.User `json:"creator"` + ID int32 `json:"id"` + Location types.Location `json:"location"` + Pool reviewTaskPool `json:"pool"` + Reviewed *time.Time `json:"addressed"` + Reviewer *platform.User `json:"addressor"` +} +type reviewTaskPool struct { + Condition string `json:"condition"` + Site types.Site `json:"site"` +} +type contentListReviewTask struct { + Tasks []reviewTask `json:"tasks"` + Total int32 `json:"total"` +} + +func (res *reviewTaskR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*contentListReviewTask, *nhttp.ErrorWithStatus) { + limit := 20 + if query.Limit != nil { + limit = *query.Limit + } + type _RowTotal struct { + Total int32 `db:"total"` + } + row_total, err := bob.One(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "COUNT(*) AS total", + ), + sm.From("review_task"), + sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID))), + sm.Where(psql.Quote("review_task", "reviewed").IsNull()), + ), scan.StructMapper[_RowTotal]()) + if err != nil { + return nil, nhttp.NewError("failed to total count: %w", err) + } + + type _Row struct { + Address types.Address `db:"address"` + Condition string `db:"condition"` + Created time.Time `db:"created"` + CreatorID int32 `db:"creator_id"` + ID int32 `db:"id"` + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` + Reviewed *time.Time `db:"reviewed"` + ReviewerID *int32 `db:"reviewer_id"` + Species *string `db:"species"` + Title string `db:"title"` + Type string `db:"type"` + } + rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "feature_pool.condition AS condition", + "review_task.created AS created", + "review_task.creator_id AS creator_id", + "review_task.id AS id", + "review_task.reviewed AS reviewed", + "review_task.reviewer_id AS reviewer_id", + "address.country AS \"address.country\"", + "address.locality AS \"address.locality\"", + "address.number_ AS \"address.number\"", + "address.postal_code AS \"address.postal_code\"", + "address.region AS \"address.region\"", + "address.street AS \"address.street\"", + "address.unit AS \"address.unit\"", + "ST_Y(address.location) AS latitude", + "ST_X(address.location) AS longitude", + ), + sm.From("review_task_pool"), + sm.InnerJoin("feature_pool").OnEQ( + psql.Quote("review_task_pool", "feature_pool_id"), + psql.Quote("feature_pool", "feature_id"), + ), + sm.InnerJoin("review_task").OnEQ( + psql.Quote("review_task_pool", "review_task_id"), + psql.Quote("review_task", "id"), + ), + sm.InnerJoin("feature").OnEQ( + psql.Quote("feature_pool", "feature_id"), + psql.Quote("feature", "id"), + ), + sm.InnerJoin("site").On( + psql.Quote("feature", "site_id").EQ(psql.Quote("site", "id")), + ), + sm.InnerJoin("address").OnEQ( + psql.Quote("site", "address_id"), + psql.Quote("address", "id"), + ), + sm.Where(psql.Quote("review_task", "organization_id").EQ(psql.Arg(user.Organization.ID))), + sm.Where(psql.Quote("review_task", "reviewed").IsNull()), + sm.Limit(limit), + ), scan.StructMapper[_Row]()) + if err != nil { + return nil, nhttp.NewError("failed to get review tasks: %w", err) + } + users_by_id, err := platform.UsersByOrg(ctx, user.Organization) + if err != nil { + return nil, nhttp.NewError("users by id: %w", err) + } + tasks := make([]reviewTask, len(rows)) + for i, row := range rows { + tasks[i] = reviewTask{ + Address: row.Address, + Created: row.Created, + Creator: *users_by_id[row.CreatorID], + ID: row.ID, + Location: types.Location{ + Latitude: row.Latitude, + Longitude: row.Longitude, + }, + Pool: reviewTaskPool{ + Condition: row.Condition, + }, + Reviewed: row.Reviewed, + Reviewer: userOrNil(users_by_id, row.ReviewerID), + } + } + return &contentListReviewTask{ + Tasks: tasks, + Total: row_total.Total, + }, nil +} +func userOrNil(usersByID map[int32]*platform.User, id *int32) *platform.User { + if id == nil { + return nil + } + u, ok := usersByID[*id] + if !ok { + return nil + } + return u +} diff --git a/resource/signal.go b/resource/signal.go new file mode 100644 index 00000000..3979f167 --- /dev/null +++ b/resource/signal.go @@ -0,0 +1,39 @@ +package resource + +import ( + "context" + "net/http" + + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + //"github.com/aarondl/opt/null" + "github.com/gorilla/mux" +) + +type signalR struct { + router *mux.Router +} + +func Signal(r *mux.Router) *signalR { + return &signalR{ + router: r, + } +} + +type contentListSignal struct { + Signals []*platform.Signal `json:"signals"` +} + +func (res *signalR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*contentListSignal, *nhttp.ErrorWithStatus) { + limit := 20 + if query.Limit != nil { + limit = *query.Limit + } + signals, err := platform.SignalList(ctx, user, limit) + if err != nil { + return nil, nhttp.NewError("list signals: %w", err) + } + return &contentListSignal{ + Signals: signals, + }, nil +} diff --git a/resource/upload.go b/resource/upload.go new file mode 100644 index 00000000..53ea1c0c --- /dev/null +++ b/resource/upload.go @@ -0,0 +1,130 @@ +package resource + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/file" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +type uploadR struct { + router *mux.Router +} + +func Upload(r *mux.Router) *uploadR { + return &uploadR{ + router: r, + } +} + +func (res *uploadR) ByIDGet(ctx context.Context, r *http.Request, u platform.User, query QueryParams) (*platform.Upload, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + file_id_str := vars["id"] + file_id_, err := strconv.ParseInt(file_id_str, 10, 32) + if err != nil { + return nil, nhttp.NewError("Failed to parse file_id: %w", err) + } + file_id := int32(file_id_) + detail, err := platform.GetUploadDetail(ctx, u.Organization.ID, file_id) + if err != nil { + return nil, nhttp.NewError("Failed to get pool: %w", err) + } + return detail, nil +} + +type contentUploadList struct { + RecentUploads []platform.Upload +} +type contentUploadPlaceholder struct{} + +func (res *uploadR) List(ctx context.Context, r *http.Request, user platform.User, req QueryParams) (*contentUploadPoolList, *nhttp.ErrorWithStatus) { + rows, err := platform.UploadList(ctx, user.Organization) + if err != nil { + return nil, nhttp.NewError("Get upload list: %w", err) + } + return &contentUploadPoolList{ + Uploads: rows, + }, nil +} + +type contentUploadDetail struct { + CSVFileID int32 + Organization platform.Organization + Upload platform.Upload +} +type contentUploadPoolList struct { + Uploads []platform.Upload `json:"uploads"` +} + +type FormUploadCommit struct{} + +func (res *uploadR) Commit(ctx context.Context, r *http.Request, u platform.User, f FormUploadCommit) (string, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + file_id_str := vars["id"] + file_id_, err := strconv.ParseInt(file_id_str, 10, 32) + if err != nil { + return "", nhttp.NewError("Failed to parse file_id: %w", err) + } + err = platform.UploadCommit(ctx, u.Organization, int32(file_id_), u) + if err != nil { + return "", nhttp.NewError("Failed to mark committed: %w", err) + } + log.Debug().Int64("file_id", file_id_).Int("user_id", u.ID).Msg("Committed file") + return "/configuration/upload", nil +} + +type FormUploadDiscard struct{} + +func (res *uploadR) Discard(ctx context.Context, r *http.Request, u platform.User, f FormUploadDiscard) (string, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + file_id_str := vars["id"] + file_id_, err := strconv.ParseInt(file_id_str, 10, 32) + if err != nil { + return "", nhttp.NewError("Failed to parse file_id: %w", err) + } + err = platform.UploadDiscard(ctx, u.Organization, int32(file_id_)) + if err != nil { + return "", nhttp.NewError("Failed to mark discarded: %w", err) + } + return "/configuration/upload", nil +} + +func (res *uploadR) PoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { + // If the organization we're uploading to doesn't have a service area, we can't process the upload correctly + if !(u.Organization.HasServiceArea() || u.Organization.IsCatchall()) { + return "", nhttp.NewErrorStatus(http.StatusConflict, "Your organization does not yet have a service area") + } + if len(uploads) == 0 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") + } + if len(uploads) != 1 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") + } + upload := uploads[0] + saved_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypeFlyover) + if err != nil { + return "", nhttp.NewError("Failed to create new pool: %w", err) + } + return fmt.Sprintf("/configuration/upload/%d", *saved_upload), nil +} +func (res *uploadR) PoolCustomCreate(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { + if len(uploads) == 0 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "No upload found") + } + if len(uploads) != 1 { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "You must only submit one file at a time") + } + upload := uploads[0] + pool_upload, err := platform.NewUpload(r.Context(), u, upload, enums.FileuploadCsvtypePoollist) + if err != nil { + return "", nhttp.NewError("Failed to create new pool: %w", err) + } + return fmt.Sprintf("/configuration/upload/%d", *pool_upload), nil +} diff --git a/resource/user.go b/resource/user.go new file mode 100644 index 00000000..6e95f34b --- /dev/null +++ b/resource/user.go @@ -0,0 +1,135 @@ +package resource + +import ( + "context" + "net/http" + "strconv" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/html" + nhttp "github.com/Gleipnir-Technology/nidus-sync/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +type userR struct { + router *mux.Router +} + +func NewUser(r *mux.Router) *userR { + return &userR{ + router: r, + } +} + +type responseListUser struct { + Users []*platform.User `json:"users"` +} +type contentURLAPI struct { + Avatar string `json:"avatar"` + Communication string `json:"communication"` + PublicreportMessage string `json:"publicreport_message"` + ReviewTask string `json:"review_task"` + Signal string `json:"signal"` + Upload string `json:"upload"` + User string `json:"user"` +} +type contentURLs struct { + API contentURLAPI `json:"api"` + Tegola string `json:"tegola"` + Tile string `json:"tile"` +} +type contentUserSelf struct { + Self platform.User `json:"self"` + URLs contentURLs `json:"urls"` +} + +func (res *userR) ByIDGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*platform.User, *nhttp.ErrorWithStatus) { + vars := mux.Vars(r) + user_id_str := vars["id"] + user_id, err := strconv.Atoi(user_id_str) + u, err := platform.UserByID(ctx, int32(user_id)) + if err != nil { + return nil, nhttp.NewError("get user: %w", err) + } + return u, nil +} + +func (res *userR) SelfGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*contentUserSelf, *nhttp.ErrorWithStatus) { + counts, err := platform.NotificationCountsForUser(ctx, user) + if err != nil { + return nil, nhttp.NewError("get notifications: %w", err) + } + org, err := platform.OrganizationByID(ctx, int(user.Organization.ID)) + if err != nil { + return nil, nhttp.NewError("get org: %w", err) + } + user.Organization = *org + user.NotificationCounts = *counts + urls := html.NewContentURL() + return &contentUserSelf{ + Self: user, + URLs: contentURLs{ + API: contentURLAPI{ + Avatar: config.MakeURLNidus("/api/avatar"), + Communication: urls.API.Communication, + PublicreportMessage: urls.API.Publicreport.Message, + ReviewTask: config.MakeURLNidus("/api/review-task"), + Signal: config.MakeURLNidus("/api/signal"), + Upload: config.MakeURLNidus("/api/upload"), + User: config.MakeURLNidus("/api/user"), + }, + Tegola: urls.Tegola, + Tile: config.MakeURLNidus("/api/tile/{z}/{y}/{x}"), + }, + }, nil +} + +func (res *userR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*responseListUser, *nhttp.ErrorWithStatus) { + users, err := platform.UsersByOrg(ctx, user.Organization) + if err != nil { + return nil, nhttp.NewError("list users: %w", err) + } + results := make([]*platform.User, len(users)) + i := 0 + for _, v := range users { + results[i] = v + i++ + } + return &responseListUser{ + Users: results, + }, nil +} + +type responseListUserSuggestion struct { + Users []*platform.User `json:"users"` +} + +func (res *userR) SuggestionGet(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*responseListUserSuggestion, *nhttp.ErrorWithStatus) { + if query.Query == nil { + return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "you need to include a query") + } + users, err := platform.UserSuggestion(ctx, user, *query.Query) + if err != nil { + return nil, nhttp.NewError("query suggestions: %w", err) + } + return &responseListUserSuggestion{ + Users: users, + }, nil +} + +func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.User, updates platform.UserChangeRequest) (string, *nhttp.ErrorWithStatus) { + log.Info().Str("avatar", updates.Avatar).Msg("doing updates") + vars := mux.Vars(r) + user_id_str := vars["id"] + user_id, err := strconv.Atoi(user_id_str) + if err != nil { + return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user update: %w", err) + } + err = platform.UserUpdate(ctx, user, user_id, updates) + if err != nil { + return "", nhttp.NewError("user update: %w", err) + } + return "", nil +}