Artifact a2a5fa22cfe97502019681748240c3c371f72a1e7b5346f43272e2d3ad869ad4:
- File
internal/api/router.go
— part of check-in
[450a8ad0f3]
at
2026-08-17 20:42:49
on branch trunk
— index: denormalize the timeline into an event table
The timeline assembled itself at read time from a five-way UNION ALL, ending in USE TEMP B-TREE FOR ORDER BY — every request sorted every event in the repository to return fifty. tk cost 536ms; fossil serves the same page in 3-4ms whatever the repository size.
This is fossil's shape: one event table with a kind column and an index on date, so the read is an index walk that stops at the limit. Triggers maintain it, like search_fts, so it cannot drift from its sources. isleaf and the ticket label are resolved for the returned page only.
tk 536ms -> 4ms sqlite 210ms -> 5ms fossil 163ms -> 3ms pikchr 22ms -> 1ms home 399ms -> 3ms (it asked each repo for its newest event)
plan_test asserts the plan, not the types: nothing about the old query was ill-typed, and the defect was only ever visible in EXPLAIN.
Also fixes limit being ignored on every wildcard route: the router's second decode pass re-applied defaults to fields the path did not name, so ?limit=200 became 50. OverlayValues sets only what it names. (user: dmestas, size: 4891) [annotate] [blame] [check-ins using]
// router.go — the API consumer of the registry (0001 §9). Every registered // op is mounted on a stdlib ServeMux; input crosses through the shared // codecs and validator, output leaves as JSON. The generated OpenAPI // document is served from the same registry at /api/v1/openapi.json. package api import ( "encoding/json" "errors" "fmt" "log" "mime" "net/http" "net/url" "reflect" "strings" "craftdesign.group/quarry/internal/ops" ) // Verify checks one repo credential pair against the capability the op // requires. A nil Verify leaves every gated op open (single-user local // serving); the composition root supplies the OLTP-backed implementation. // need is the op's own ops.Auth, so one seam serves both the write ops // (Token) and the administrative ones (Admin) without the router // deciding what either means. type Verify func(repo, user, password string, need ops.Auth) bool // Router mounts every op in r plus the OpenAPI document. version flows // into the spec's info block. verify (optional) gates ops.Token ops // with HTTP Basic against it. func Router(r *ops.Registry, version string, verify ...Verify) (http.Handler, error) { var v Verify if len(verify) > 0 { v = verify[0] } mux := http.NewServeMux() for _, d := range r.All() { mux.Handle(d.Method+" "+d.Pattern, opHandler(r, d, v)) } spec, err := ops.Spec(r, "quarry", version) if err != nil { return nil, err } specJSON, err := json.MarshalIndent(spec, "", " ") if err != nil { return nil, err } mux.HandleFunc("GET /api/v1/openapi.json", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write(specJSON) }) // Unmatched API paths answer in the API's own dialect, never Go's // plain-text default. mux.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { writeError(w, http.StatusNotFound, errors.New("no such endpoint")) }) return mux, nil } func opHandler(r *ops.Registry, d *ops.Descriptor, verify Verify) http.Handler { wild := ops.Wildcards(d.Pattern) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if d.Auth != ops.Anonymous && verify != nil { user, pass, _ := req.BasicAuth() if !verify(req.PathValue("repo"), user, pass, d.Auth) { w.Header().Set("WWW-Authenticate", `Basic realm="quarry"`) writeError(w, http.StatusUnauthorized, errors.New("authentication required")) return } req = req.WithContext(ops.WithUser(req.Context(), user)) } in, err := decodeIn(req, d, wild) if err != nil { writeError(w, http.StatusBadRequest, err) return } if err := ops.Validate(in); err != nil { writeError(w, http.StatusBadRequest, err) return } out, err := r.Invoke(req.Context(), d.Name, in) if errors.Is(err, ops.ErrNotFound) { writeError(w, http.StatusNotFound, err) return } if errors.Is(err, ops.ErrInvalid) { writeError(w, http.StatusBadRequest, err) return } if errors.Is(err, ops.ErrUnavailable) { writeError(w, http.StatusServiceUnavailable, err) return } if err != nil { // The real error goes to the log; the public body never // carries internals (subprocess invocations, SQL, paths). log.Printf("api: %s: %v", d.Name, err) writeError(w, http.StatusInternalServerError, errors.New("internal error")) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(out) }) } // decodeIn assembles the op's In struct from the request: query string // (GET) or body (POST, form or JSON by content type), with path wildcards // merged last so the routed path is always authoritative. func decodeIn(req *http.Request, d *ops.Descriptor, wild []string) (any, error) { inPtr := d.NewIn() switch { case d.Method == http.MethodGet: if err := ops.DecodeValues(req.URL.Query(), inPtr); err != nil { return nil, err } case isJSON(req): if err := json.NewDecoder(req.Body).Decode(inPtr); err != nil { return nil, fmt.Errorf("body: %w", err) } default: if err := req.ParseForm(); err != nil { return nil, fmt.Errorf("form: %w", err) } if err := ops.DecodeValues(req.PostForm, inPtr); err != nil { return nil, err } } if len(wild) > 0 { pathVals := url.Values{} for _, name := range wild { pathVals.Set(name, req.PathValue(name)) } if err := ops.OverlayValues(pathVals, inPtr); err != nil { return nil, err } } return deref(inPtr), nil } func deref(p any) any { return reflect.ValueOf(p).Elem().Interface() } func isJSON(req *http.Request) bool { ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")) return strings.HasSuffix(ct, "/json") || strings.HasSuffix(ct, "+json") } func writeError(w http.ResponseWriter, code int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) }