feat(05-01): add Paginate envelope and RegisterJoinTable
- Page/PageMeta emit {data, meta} with no links key
- RegisterJoinTable fails loud on nil db; document pivot-write contract
This commit is contained in:
40
lagoon/paginate.go
Normal file
40
lagoon/paginate.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package lagoon
|
||||
|
||||
// PageMeta is the pagination block of the house REST envelope (DATA-10).
|
||||
type PageMeta struct {
|
||||
CurrentPage int `json:"current_page"`
|
||||
LastPage int `json:"last_page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// Page wraps already-built row DTOs. There is no links field.
|
||||
type Page[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Meta PageMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// Paginate builds {data, meta{current_page,last_page,per_page,total}} from
|
||||
// a caller-sliced row set. LastPage is ceil(total/perPage); perPage<=0
|
||||
// yields LastPage 1 instead of dividing by zero. Nil rows become [].
|
||||
func Paginate[T any](rows []T, page, perPage int, total int64) Page[T] {
|
||||
last := 1
|
||||
if perPage > 0 {
|
||||
last = int((total + int64(perPage) - 1) / int64(perPage))
|
||||
if last < 1 {
|
||||
last = 1
|
||||
}
|
||||
}
|
||||
if rows == nil {
|
||||
rows = []T{}
|
||||
}
|
||||
return Page[T]{
|
||||
Data: rows,
|
||||
Meta: PageMeta{
|
||||
CurrentPage: page,
|
||||
LastPage: last,
|
||||
PerPage: perPage,
|
||||
Total: total,
|
||||
},
|
||||
}
|
||||
}
|
||||
42
lagoon/paginate_test.go
Normal file
42
lagoon/paginate_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPaginateEnvelope(t *testing.T) {
|
||||
got := Paginate([]string{"a", "b", "c"}, 1, 2, 3)
|
||||
if got.Meta.CurrentPage != 1 || got.Meta.LastPage != 2 || got.Meta.PerPage != 2 || got.Meta.Total != 3 {
|
||||
t.Fatalf("meta = %+v", got.Meta)
|
||||
}
|
||||
raw, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "links") {
|
||||
t.Fatalf("envelope must not contain links: %s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"data"`) || !strings.Contains(string(raw), `"meta"`) {
|
||||
t.Fatalf("missing data/meta: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginateZeroPerPage(t *testing.T) {
|
||||
got := Paginate([]int{1}, 1, 0, 10)
|
||||
if got.Meta.LastPage != 1 {
|
||||
t.Fatalf("last_page = %d, want 1", got.Meta.LastPage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginateNilDataIsEmptyArray(t *testing.T) {
|
||||
got := Paginate[string](nil, 1, 10, 0)
|
||||
raw, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"data":[]`) {
|
||||
t.Fatalf("nil rows must marshal as []: %s", raw)
|
||||
}
|
||||
}
|
||||
28
lagoon/relations.go
Normal file
28
lagoon/relations.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterJoinTable wraps db.SetupJoinTable so a nil db returns an error
|
||||
// instead of panicking.
|
||||
//
|
||||
// Pivot-write contract: pivot tables with business columns (sort_order,
|
||||
// role, granted_at, granted_by) are never written through
|
||||
// db.Model(&owner).Association(field).Append/Replace(...). GORM's
|
||||
// Association Mode has no documented path to set those columns. Writes go
|
||||
// through an explicit delete-then-bulk-insert (or ON CONFLICT DO UPDATE)
|
||||
// against the join table directly, in the same transaction as the parent
|
||||
// save. Reads use Preload(field) plus .Order(...) on the pivot's own
|
||||
// columns once RegisterJoinTable has run.
|
||||
func RegisterJoinTable(db *gorm.DB, owner any, field string, joinModel any) error {
|
||||
if db == nil {
|
||||
return fmt.Errorf("lagoon: register join table: db is nil")
|
||||
}
|
||||
if err := db.SetupJoinTable(owner, field, joinModel); err != nil {
|
||||
return fmt.Errorf("lagoon: register join table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
16
lagoon/relations_test.go
Normal file
16
lagoon/relations_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package lagoon
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegisterJoinTableNilDB(t *testing.T) {
|
||||
err := RegisterJoinTable(nil, struct{}{}, "Artists", struct{}{})
|
||||
if err == nil {
|
||||
t.Fatal("want non-nil error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "nil") {
|
||||
t.Fatalf("got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user