diff --git a/internal/handlers/catalogs.go b/internal/handlers/catalogs.go index 3bfba48..ac92378 100644 --- a/internal/handlers/catalogs.go +++ b/internal/handlers/catalogs.go @@ -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" @@ -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. @@ -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 }, + }) } diff --git a/internal/handlers/crud.go b/internal/handlers/crud.go index 5500ba8..428a393 100644 --- a/internal/handlers/crud.go +++ b/internal/handlers/crud.go @@ -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" @@ -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: " 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. diff --git a/internal/handlers/software.go b/internal/handlers/software.go index f6400b0..6185cf9 100644 --- a/internal/handlers/software.go +++ b/internal/handlers/software.go @@ -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" @@ -326,11 +323,7 @@ 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. @@ -338,40 +331,23 @@ func (p *Software) GetSoftwareAnalysis(ctx *fiber.Ctx) error { 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