package main import ( "context" "encoding/json" "errors" "log" "net/http" "net/url" "strings" "time" ) // Saved search routes: // // GET /api/saved list // POST /api/saved create (and run it once in the background) // GET /api/saved/{id} // PUT /api/saved/{id} update // DELETE /api/saved/{id} // POST /api/saved/{id}/run run now // GET /api/saved/{id}/runs history (summaries) // GET /api/saved/{id}/runs/{run} one snapshot ("latest" accepted) func (a *API) savedRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/saved", a.savedList) mux.HandleFunc("POST /api/saved", a.savedCreate) mux.HandleFunc("GET /api/saved/{id}", a.savedGet) mux.HandleFunc("PUT /api/saved/{id}", a.savedUpdate) mux.HandleFunc("DELETE /api/saved/{id}", a.savedDelete) mux.HandleFunc("POST /api/saved/{id}/run", a.savedRun) mux.HandleFunc("GET /api/saved/{id}/runs", a.savedRuns) mux.HandleFunc("GET /api/saved/{id}/runs/{run}", a.savedRunGet) } // savedInput is what the UI sends on create and update. type savedInput struct { Name string `json:"name"` Params string `json:"params"` Schedule Schedule `json:"schedule"` MaxResults int `json:"maxResults"` Enabled *bool `json:"enabled"` } func (in *savedInput) validate() error { in.Name = strings.TrimSpace(in.Name) if in.Name == "" || len(in.Name) > 120 { return &codedError{code: "bad_param", msg: "invalid name", detail: "name"} } v, err := url.ParseQuery(in.Params) if err != nil { return &codedError{code: "bad_param", msg: "invalid parameters", detail: "params"} } v.Del("page") v.Del("per_page") in.Params = v.Encode() if _, err := ParseCriteria(v); err != nil { return err } switch in.MaxResults { case 0: in.MaxResults = 50 case 30, 50, 100: default: return &codedError{code: "bad_param", msg: "invalid maxResults", detail: "maxResults"} } if !in.Schedule.Valid() { return &codedError{code: "bad_param", msg: "invalid schedule", detail: "schedule"} } return nil } func (a *API) savedList(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, a.store.List()) } func (a *API) savedGet(w http.ResponseWriter, r *http.Request) { x, err := a.store.Get(r.PathValue("id")) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, x) } func readInput(r *http.Request) (savedInput, error) { var in savedInput dec := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 64<<10)) if err := dec.Decode(&in); err != nil { return in, &codedError{code: "bad_param", msg: "invalid JSON", detail: err.Error()} } return in, in.validate() } func (a *API) savedCreate(w http.ResponseWriter, r *http.Request) { in, err := readInput(r) if err != nil { writeErr(w, http.StatusBadRequest, err) return } x := SavedSearch{Name: in.Name, Params: in.Params, Schedule: in.Schedule, MaxResults: in.MaxResults, Enabled: true} if in.Enabled != nil { x.Enabled = *in.Enabled } x.NextRunAt = x.Schedule.Next(time.Now(), time.Time{}) if !x.Enabled { x.NextRunAt = time.Time{} } x, err = a.store.Create(x) if err != nil { writeErr(w, http.StatusInternalServerError, err) return } // First run right away: it gives a baseline to compare the next ones with. go func(id string) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() if _, err := a.sched.Run(ctx, id, "manual"); err != nil { log.Printf("first run of saved search %s: %v", id, err) } }(x.ID) writeJSON(w, http.StatusCreated, x) } func (a *API) savedUpdate(w http.ResponseWriter, r *http.Request) { in, err := readInput(r) if err != nil { writeErr(w, http.StatusBadRequest, err) return } x, err := a.store.Update(r.PathValue("id"), func(v *SavedSearch) { v.Name, v.Params, v.Schedule, v.MaxResults = in.Name, in.Params, in.Schedule, in.MaxResults if in.Enabled != nil { v.Enabled = *in.Enabled } v.NextRunAt = time.Time{} if v.Enabled { v.NextRunAt = v.Schedule.Next(time.Now(), v.LastRunAt) } }) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, x) } func (a *API) savedDelete(w http.ResponseWriter, r *http.Request) { if err := a.store.Delete(r.PathValue("id")); err != nil { writeStoreErr(w, err) return } w.WriteHeader(http.StatusNoContent) } func (a *API) savedRun(w http.ResponseWriter, r *http.Request) { run, err := a.sched.Run(r.Context(), r.PathValue("id"), "manual") if err != nil && run.ID == "" { writeStoreErr(w, err) return } // A failed GitHub search is still a recorded run: the error is in run.error. writeJSON(w, http.StatusOK, run.RunInfo) } func (a *API) savedRuns(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, err := a.store.Get(id); err != nil { writeStoreErr(w, err) return } runs, err := a.store.Runs(id) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, runs) } func (a *API) savedRunGet(w http.ResponseWriter, r *http.Request) { run, err := a.store.Run(r.PathValue("id"), r.PathValue("run")) if err != nil { writeStoreErr(w, err) return } writeJSON(w, http.StatusOK, run) } func writeStoreErr(w http.ResponseWriter, err error) { status := http.StatusInternalServerError var ce *codedError if errors.As(err, &ce) { switch ce.code { case "not_found": status = http.StatusNotFound case "bad_param": status = http.StatusBadRequest } } writeErr(w, status, err) }