package api import ( "net/http" "strconv" "strings" "reanimator/internal/repository/timeline" ) func writeTimelineResponse(w http.ResponseWriter, r *http.Request, repo *timeline.EventRepository, subjectType string, subjectID string) { limit := 50 if raw := r.URL.Query().Get("limit"); raw != "" { if parsed, err := strconv.Atoi(raw); err == nil { limit = parsed } else { writeError(w, http.StatusBadRequest, "invalid limit") return } } var cursor *timeline.Cursor if raw := r.URL.Query().Get("cursor"); raw != "" { decoded, err := timeline.DecodeCursor(raw) if err != nil { writeError(w, http.StatusBadRequest, "invalid cursor") return } cursor = &decoded } items, next, err := repo.List(r.Context(), subjectType, subjectID, limit, cursor) if err != nil { writeError(w, http.StatusInternalServerError, "list timeline failed") return } var nextCursor *string if next != nil { value := timeline.EncodeCursor(*next) nextCursor = &value } writeJSON(w, http.StatusOK, map[string]any{ "items": items, "next_cursor": nextCursor, }) } func writeImportHistoryResponse(w http.ResponseWriter, r *http.Request, repo *timeline.EventRepository, subjectType string, subjectID string) { limit := 20 if raw := r.URL.Query().Get("limit"); raw != "" { if parsed, err := strconv.Atoi(raw); err == nil { limit = parsed } else { writeError(w, http.StatusBadRequest, "invalid limit") return } } var cursor *timeline.Cursor if raw := r.URL.Query().Get("cursor"); raw != "" { decoded, err := timeline.DecodeCursor(raw) if err != nil { writeError(w, http.StatusBadRequest, "invalid cursor") return } cursor = &decoded } var ( items []timeline.ImportHistoryItem next *timeline.Cursor err error ) switch subjectType { case "asset": items, next, err = repo.ListAssetImportHistory(r.Context(), subjectID, limit, cursor) case "component": items, next, err = repo.ListComponentImportHistory(r.Context(), subjectID, limit, cursor) default: writeError(w, http.StatusBadRequest, "unsupported subject type") return } if err != nil { writeError(w, http.StatusInternalServerError, "list import history failed") return } var nextCursor *string if next != nil { value := timeline.EncodeCursor(*next) nextCursor = &value } writeJSON(w, http.StatusOK, map[string]any{ "items": items, "next_cursor": nextCursor, }) } func parseTimelineID(path, prefix string) string { if !strings.HasPrefix(path, prefix) || !strings.HasSuffix(path, "/timeline") { return "" } trimmed := strings.TrimPrefix(path, prefix) trimmed = strings.TrimSuffix(trimmed, "/timeline") if trimmed == "" || strings.Contains(trimmed, "/") { return "" } if !entityIDPattern.MatchString(trimmed) { return "" } return trimmed }