Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 6 additions & 32 deletions internal/handlers/catalogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package handlers

import (
"cmp"
"encoding/json"
"errors"
"net/url"
"slices"
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
"github.com/publiccodeyml/open-catalog-api/internal/common"
"github.com/publiccodeyml/open-catalog-api/internal/database"
"github.com/publiccodeyml/open-catalog-api/internal/handlers/general"
"github.com/publiccodeyml/open-catalog-api/internal/models"
"gorm.io/gorm"
Expand Down Expand Up @@ -576,11 +573,7 @@ func (c *Catalog) GetCatalogAnalysis(ctx *fiber.Ctx) error {
return common.Error(fiber.StatusNotFound, errMsg, "Catalog was not found")
}

if catalog.Analysis == nil {
return ctx.JSON(common.AnalysisData{})
}

return ctx.JSON(catalog.Analysis)
return ctx.JSON(analysisOrEmpty(catalog.Analysis))
}

// PatchCatalogAnalysis merges the incoming analysis namespaces into the stored analysis.
Expand All @@ -599,28 +592,9 @@ func (c *Catalog) PatchCatalogAnalysis(ctx *fiber.Ctx) error {
return common.Error(fiber.StatusNotFound, errMsg, "Catalog was not found")
}

var incoming common.AnalysisData
if err := json.Unmarshal(ctx.Body(), &incoming); err != nil {
return common.Error(fiber.StatusUnprocessableEntity, errMsg, "invalid or malformed JSON")
}

patch, err := common.WithTimestamps(incoming, time.Now())
if err != nil {
return common.Error(fiber.StatusUnprocessableEntity, errMsg, err.Error())
}

if len(patch) == 0 {
return ctx.JSON(analysisOrEmpty(catalog.Analysis))
}

merged, err := database.MergeAnalysis(writeDB(ctx, c.db), catalog, patch)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.Error(fiber.StatusNotFound, errMsg, "Catalog was not found")
}

return common.InternalServerError(errMsg)
}

return ctx.JSON(merged)
return patchAnalysis(ctx, c.db, catalog, analysisOptions{
title: errMsg,
name: "Catalog",
current: func() (common.AnalysisData, error) { return catalog.Analysis, nil },
})
}
53 changes: 53 additions & 0 deletions internal/handlers/crud.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package handlers

import (
"encoding/json"
"errors"
"time"

"github.com/gofiber/fiber/v2"
"github.com/pilagod/gorm-cursor-paginator/v2/paginator"
"github.com/publiccodeyml/open-catalog-api/internal/common"
"github.com/publiccodeyml/open-catalog-api/internal/database"
"github.com/publiccodeyml/open-catalog-api/internal/handlers/general"
"github.com/publiccodeyml/open-catalog-api/internal/models"
"gorm.io/gorm"
Expand Down Expand Up @@ -157,6 +160,56 @@ func analysisOrEmpty(a common.AnalysisData) common.AnalysisData {
return a
}

// analysisOptions drives patchAnalysis.
type analysisOptions struct {
// title is the Problem JSON title on failure, e.g. "can't update Software analysis".
title string
// name is the resource name in the 404 detail: "<name> was not found".
name string
// current answers the analysis the row holds. It runs only for an empty
// patch: a handler that leaves the row to the merge, addressed by its
// primary key, reads nothing on a request that writes.
current func() (common.AnalysisData, error)
}

// patchAnalysis runs the PATCH sequence shared by the analysis endpoints:
// decode the body, stamp every namespace with its timestamp, answer the
// stored document when the patch is empty and merge it into model
// otherwise. Errors are Problem JSON errors, ready to be returned from a
// handler as they are.
func patchAnalysis(ctx *fiber.Ctx, gormdb *gorm.DB, model models.Analyzable, opts analysisOptions) error {
var incoming common.AnalysisData
if err := json.Unmarshal(ctx.Body(), &incoming); err != nil {
return common.Error(fiber.StatusUnprocessableEntity, opts.title, "invalid or malformed JSON")
}

patch, err := common.WithTimestamps(incoming, time.Now())
if err != nil {
return common.Error(fiber.StatusUnprocessableEntity, opts.title, err.Error())
}

// An empty patch writes nothing: no updated_at bump, no event, no webhook.
if len(patch) == 0 {
current, err := opts.current()
if err != nil {
return err
}

return ctx.JSON(analysisOrEmpty(current))
}

merged, err := database.MergeAnalysis(writeDB(ctx, gormdb), model, patch)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.Error(fiber.StatusNotFound, opts.title, opts.name+" was not found")
}

return common.InternalServerError(opts.title)
}

return ctx.JSON(merged)
}

// writeError maps the error of a create or update to Problem JSON: 409
// when a unique constraint or the alternativeId check failed, 500
// otherwise. The raw database error never reaches the client.
Expand Down
54 changes: 15 additions & 39 deletions internal/handlers/software.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package handlers

import (
"encoding/json"
"errors"
"slices"
"sort"
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
"github.com/publiccodeyml/open-catalog-api/internal/common"
"github.com/publiccodeyml/open-catalog-api/internal/database"
"github.com/publiccodeyml/open-catalog-api/internal/handlers/general"
"github.com/publiccodeyml/open-catalog-api/internal/models"
"gorm.io/gorm"
Expand Down Expand Up @@ -326,52 +323,31 @@ func (p *Software) GetSoftwareAnalysis(ctx *fiber.Ctx) error {
return err
}

if software.Analysis == nil {
return ctx.JSON(common.AnalysisData{})
}

return ctx.JSON(software.Analysis)
return ctx.JSON(analysisOrEmpty(software.Analysis))
}

// PatchSoftwareAnalysis merges the incoming analysis namespaces into the stored analysis.
// The request body is an AnalysisData object (namespace → arbitrary JSON with "v" field).
func (p *Software) PatchSoftwareAnalysis(ctx *fiber.Ctx) error {
const errMsg = "can't update Software analysis"

var incoming common.AnalysisData
if err := json.Unmarshal(ctx.Body(), &incoming); err != nil {
return common.Error(fiber.StatusUnprocessableEntity, errMsg, "invalid or malformed JSON")
}

patch, err := common.WithTimestamps(incoming, time.Now())
if err != nil {
return common.Error(fiber.StatusUnprocessableEntity, errMsg, err.Error())
}

if len(patch) == 0 {
software, err := findOne[models.Software](p.db, ctx.Params("id"), findOptions{
title: errMsg,
name: softwareEntityName,
})
if err != nil {
return err
}

return ctx.JSON(analysisOrEmpty(software.Analysis))
}

software := models.Software{ID: ctx.Params("id")}

merged, err := database.MergeAnalysis(writeDB(ctx, p.db), &software, patch)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.Error(fiber.StatusNotFound, errMsg, "Software was not found")
}

return common.InternalServerError(errMsg)
}
return patchAnalysis(ctx, p.db, &software, analysisOptions{
title: errMsg,
name: softwareEntityName,
current: func() (common.AnalysisData, error) {
found, err := findOne[models.Software](p.db, software.ID, findOptions{
title: errMsg,
name: softwareEntityName,
})
if err != nil {
return nil, err
}

return ctx.JSON(merged)
return found.Analysis, nil
},
})
}

// softwareURLFilter narrows stmt to the software owning the ?url query
Expand Down
Loading