617 lines
18 KiB
Go
617 lines
18 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/catalog"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/feeds"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||
|
|
"github.com/go-chi/chi/v5"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (s *Server) handleListFeeds(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
limit, offset := ParseLimitOffset(r)
|
||
|
|
page, total, activeTotal, mappedTotal, err := s.Feeds.List(r.Context(), cid, limit, offset, QuerySearch(r))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
products, err := s.Feeds.CompanyProductTotals(r.Context(), cid)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{
|
||
|
|
"feeds": feeds.PresentFeeds(page), "total": total, "active_total": activeTotal, "mapped_total": mappedTotal,
|
||
|
|
"product_total": products.Total, "processed_total": products.Processed, "unprocessed_total": products.Unprocessed,
|
||
|
|
"limit": limit, "offset": offset,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleCreateFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
uid, _ := UserIDFromContext(r.Context())
|
||
|
|
|
||
|
|
ct := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type")))
|
||
|
|
if strings.HasPrefix(ct, "multipart/form-data") {
|
||
|
|
s.createFeedFromMultipart(w, r, cid, uid)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var body struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
URL string `json:"url"`
|
||
|
|
ItemPath string `json:"item_path"`
|
||
|
|
FeedType string `json:"feed_type"`
|
||
|
|
SyncIntervalMinutes int `json:"sync_interval_minutes"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.Create(r.Context(), cid, feeds.CreateInput{
|
||
|
|
Name: body.Name,
|
||
|
|
URL: body.URL,
|
||
|
|
ItemPath: body.ItemPath,
|
||
|
|
FeedType: body.FeedType,
|
||
|
|
SyncIntervalMinutes: body.SyncIntervalMinutes,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, feeds.PresentFeed(item))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) createFeedFromMultipart(w http.ResponseWriter, r *http.Request, cid, uid uuid.UUID) {
|
||
|
|
if err := r.ParseMultipartForm(catalogMaxUpload); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid multipart form")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
||
|
|
url := strings.TrimSpace(r.FormValue("url"))
|
||
|
|
feedType := strings.TrimSpace(r.FormValue("feed_type"))
|
||
|
|
itemPath := strings.TrimSpace(r.FormValue("item_path"))
|
||
|
|
interval, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("sync_interval_minutes")))
|
||
|
|
|
||
|
|
file, header, fileErr := r.FormFile("file")
|
||
|
|
var options map[string]any
|
||
|
|
if fileErr == nil {
|
||
|
|
defer file.Close()
|
||
|
|
meta, err := s.Catalog.SaveUpload(
|
||
|
|
r.Context(),
|
||
|
|
cid,
|
||
|
|
uid,
|
||
|
|
s.Config.UploadDir,
|
||
|
|
header.Filename,
|
||
|
|
header.Header.Get("Content-Type"),
|
||
|
|
"feed",
|
||
|
|
file,
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not save upload", err, catalog.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
pathStr, _ := meta["path"].(string)
|
||
|
|
fileID, _ := meta["id"].(string)
|
||
|
|
fileName, _ := meta["name"].(string)
|
||
|
|
options = map[string]any{
|
||
|
|
"source_path": pathStr,
|
||
|
|
"source_file_id": fileID,
|
||
|
|
"source_filename": fileName,
|
||
|
|
"source_kind": "csv",
|
||
|
|
}
|
||
|
|
if feedType == "" {
|
||
|
|
feedType = "csv"
|
||
|
|
}
|
||
|
|
if fid, err := uuid.Parse(fileID); err == nil {
|
||
|
|
_, _ = s.Catalog.UpdateFileStatus(r.Context(), cid, fid, "uploaded", map[string]any{
|
||
|
|
"kind": "feed",
|
||
|
|
"feed": true,
|
||
|
|
"name": name,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} else if url == "" && itemPath == "" {
|
||
|
|
Error(w, http.StatusBadRequest, "url or file field required")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
item, err := s.Feeds.Create(r.Context(), cid, feeds.CreateInput{
|
||
|
|
Name: name,
|
||
|
|
URL: url,
|
||
|
|
ItemPath: itemPath,
|
||
|
|
FeedType: feedType,
|
||
|
|
SyncIntervalMinutes: interval,
|
||
|
|
Options: options,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, feeds.PresentFeed(item))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleGetFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.Get(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
if feeds.IsNotFound(err) {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
Error(w, http.StatusInternalServerError, "get failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, feeds.PresentFeed(item))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleUpdateFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body map[string]any
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.Update(r.Context(), cid, id, body)
|
||
|
|
if err != nil {
|
||
|
|
if feeds.IsNotFound(err) {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, feeds.PresentFeed(item))
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleDeleteFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := s.Feeds.Delete(r.Context(), cid, id); err != nil {
|
||
|
|
if feeds.IsNotFound(err) {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
Error(w, http.StatusInternalServerError, "delete failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"id": id.String(), "deleted": true})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleSyncFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
jobID, err := s.Feeds.EnqueueSync(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
if feeds.IsNotFound(err) {
|
||
|
|
Error(w, http.StatusNotFound, "Feed not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not sync feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if s.Jobs != nil {
|
||
|
|
_ = s.Jobs.EnqueueFeedSyncJob(r.Context(), jobID)
|
||
|
|
}
|
||
|
|
job, err := s.Feeds.GetSyncJob(r.Context(), cid, id, jobID)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "could not load sync job")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusAccepted, job)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleListSyncJobs(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
limit, _ := ParseLimitOffset(r)
|
||
|
|
items, err := s.Feeds.ListSyncJobs(r.Context(), cid, id, limit)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"jobs": items, "limit": limit})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleGetSyncJob(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
feedID, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
jobID, err := uuid.Parse(chi.URLParam(r, "jobID"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid job id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
job, err := s.Feeds.GetSyncJob(r.Context(), cid, feedID, jobID)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, job)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleGetFeedMappings(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.GetMappings(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"mappings": []any{}})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handlePutFeedMappings(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
Mappings any `json:"mappings"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.PutMappings(r.Context(), cid, id, body.Mappings)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not save mappings", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleExtractFeedSchema(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
ItemPath string `json:"item_path"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSONOptional(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
result, err := s.Feeds.ExtractSchema(r.Context(), cid, id, body.ItemPath)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not extract schema", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, result)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleSyncAndProcessSample syncs a company-scoped feed then starts a processing job
|
||
|
|
// for up to N of that feed's raw products (default 10, max 100).
|
||
|
|
func (s *Server) handleSyncAndProcessSample(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
uid, _ := UserIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
Limit int `json:"limit"`
|
||
|
|
SkipSync bool `json:"skip_sync"`
|
||
|
|
ProcessingType string `json:"processing_type"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSONOptional(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
limit := body.Limit
|
||
|
|
if limit <= 0 {
|
||
|
|
limit = 10
|
||
|
|
}
|
||
|
|
if limit > 100 {
|
||
|
|
limit = 100
|
||
|
|
}
|
||
|
|
processingType := strings.TrimSpace(body.ProcessingType)
|
||
|
|
if processingType == "" {
|
||
|
|
processingType = "full"
|
||
|
|
}
|
||
|
|
|
||
|
|
var syncJob map[string]any
|
||
|
|
if !body.SkipSync {
|
||
|
|
job, syncErr := s.Feeds.Sync(r.Context(), cid, id)
|
||
|
|
if syncErr != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not sync feed", syncErr, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
syncJob = job
|
||
|
|
}
|
||
|
|
|
||
|
|
rawIDs, err := s.Catalog.ListRawProductIDsByFeed(r.Context(), cid, id, limit)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "list raw products failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if len(rawIDs) == 0 {
|
||
|
|
JSON(w, http.StatusOK, map[string]any{
|
||
|
|
"sync_job": syncJob,
|
||
|
|
"processing_job": nil,
|
||
|
|
"raw_product_ids": []string{},
|
||
|
|
"sample_requested": limit,
|
||
|
|
"sample_queued": 0,
|
||
|
|
"message": "Sync completed but no raw products found for this feed",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
procJobs, err := s.Processing.StartJob(r.Context(), cid, uid, rawIDs, processingType)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not start processing", err, processing.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
for _, procJob := range procJobs {
|
||
|
|
if err := s.Jobs.EnqueueProcessingJob(r.Context(), procJob.ID); err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "enqueue failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var primary any
|
||
|
|
if len(procJobs) > 0 {
|
||
|
|
primary = processing.FormatStartJobsResponse(procJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
idStrs := make([]string, 0, len(rawIDs))
|
||
|
|
for _, rid := range rawIDs {
|
||
|
|
idStrs = append(idStrs, rid.String())
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusAccepted, map[string]any{
|
||
|
|
"sync_job": syncJob,
|
||
|
|
"processing_job": primary,
|
||
|
|
"processing_jobs": procJobs,
|
||
|
|
"raw_product_ids": idStrs,
|
||
|
|
"sample_requested": limit,
|
||
|
|
"sample_queued": len(rawIDs),
|
||
|
|
"message": fmt.Sprintf("Queued %d product(s) for processing", len(rawIDs)),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleListExportFeeds(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
limit, offset := ParseLimitOffset(r)
|
||
|
|
page, total, err := s.Feeds.ListExportFeeds(r.Context(), cid, limit, offset)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]any{"export_feeds": page, "total": total, "limit": limit, "offset": offset})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleCreateExportFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
var body struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
SourceFeedID *string `json:"source_feed_id"`
|
||
|
|
Format string `json:"format"`
|
||
|
|
Template any `json:"template"`
|
||
|
|
Filters any `json:"filters"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.CreateExportFeed(r.Context(), cid, feeds.CreateExportInput{
|
||
|
|
Name: body.Name, SourceFeedID: body.SourceFeedID, Format: body.Format,
|
||
|
|
Template: body.Template, Filters: body.Filters,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create export feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusCreated, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleGetExportFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.GetExportFeed(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleUpdateExportFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
Name *string `json:"name"`
|
||
|
|
IsActive *bool `json:"is_active"`
|
||
|
|
Template any `json:"template"`
|
||
|
|
Filters any `json:"filters"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.UpdateExportFeed(r.Context(), cid, id, body.Name, body.IsActive, body.Template, body.Filters)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update export feed", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleUpdateExportFeedTemplate(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var body struct {
|
||
|
|
Template any `json:"template"`
|
||
|
|
Filters any `json:"filters"`
|
||
|
|
}
|
||
|
|
if err := DecodeJSON(r, &body); err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.UpdateExportFeedTemplate(r.Context(), cid, id, body.Template, body.Filters)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not update export template", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleDeleteExportFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := s.Feeds.DeleteExportFeed(r.Context(), cid, id); err != nil {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleRotateExportFeedPublicToken(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.RotateExportFeedPublicToken(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusNotFound, "not found")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleGenerateExportFeed(w http.ResponseWriter, r *http.Request) {
|
||
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
||
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||
|
|
if err != nil {
|
||
|
|
Error(w, http.StatusBadRequest, "invalid id")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := s.Feeds.GenerateExportFeed(r.Context(), cid, id)
|
||
|
|
if err != nil {
|
||
|
|
ClientOrLog(w, http.StatusBadRequest, "could not generate export", err, feeds.ClientError)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusOK, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handlePublicExportXML(w http.ResponseWriter, r *http.Request) {
|
||
|
|
token := chi.URLParam(r, "token")
|
||
|
|
lw := &lazyHeaderWriter{ResponseWriter: w, contentType: "application/xml; charset=utf-8"}
|
||
|
|
if err := s.Feeds.PublicExportXML(r.Context(), lw, token); err != nil {
|
||
|
|
if !lw.wrote {
|
||
|
|
writePublicExportError(w, err)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handlePublicExportCSV(w http.ResponseWriter, r *http.Request) {
|
||
|
|
token := chi.URLParam(r, "token")
|
||
|
|
lw := &lazyHeaderWriter{ResponseWriter: w, contentType: "text/csv; charset=utf-8"}
|
||
|
|
if err := s.Feeds.PublicExportCSV(r.Context(), lw, token); err != nil {
|
||
|
|
if !lw.wrote {
|
||
|
|
writePublicExportError(w, err)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func writePublicExportError(w http.ResponseWriter, err error) {
|
||
|
|
// Format mismatch must look identical to unknown tokens so probing .xml/.csv
|
||
|
|
// cannot confirm whether a guessed public_token exists.
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, feeds.ErrFormatMismatch), errors.Is(err, pgx.ErrNoRows):
|
||
|
|
Error(w, http.StatusNotFound, "export feed not found")
|
||
|
|
default:
|
||
|
|
Error(w, http.StatusNotFound, "export feed not found")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type lazyHeaderWriter struct {
|
||
|
|
http.ResponseWriter
|
||
|
|
contentType string
|
||
|
|
wrote bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func (l *lazyHeaderWriter) Write(p []byte) (int, error) {
|
||
|
|
if !l.wrote {
|
||
|
|
l.Header().Set("Content-Type", l.contentType)
|
||
|
|
l.wrote = true
|
||
|
|
}
|
||
|
|
return l.ResponseWriter.Write(p)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (l *lazyHeaderWriter) Flush() {
|
||
|
|
if f, ok := l.ResponseWriter.(http.Flusher); ok {
|
||
|
|
f.Flush()
|
||
|
|
}
|
||
|
|
}
|