feat(12.1-01): list row states from one controller call per page
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled - list response meta.row_states keyed by row id; unknown values dropped - list messages rowStateDeleted, rowStateNegative, rowStateDisabled - update writes through the scope the load used, so a soft-deleted record a controller includes stays soft-deleted - DataTable row state badges and text styles - roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs
This commit is contained in:
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -28,12 +30,16 @@ type ListInput struct {
|
||||
Filters map[string]string
|
||||
}
|
||||
|
||||
// ListMeta is the D-11 pagination block.
|
||||
// ListMeta is the D-11 pagination block. RowStates are the states of the
|
||||
// page's rows keyed by row id (a decimal string), each a subset of deleted,
|
||||
// negative and disabled in that order; rows without a state are left out, and
|
||||
// the key is absent when the controller reports no state at all.
|
||||
type ListMeta struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int64 `json:"total"`
|
||||
LastPage int `json:"last_page"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int64 `json:"total"`
|
||||
LastPage int `json:"last_page"`
|
||||
RowStates map[string][]string `json:"row_states,omitempty"`
|
||||
}
|
||||
|
||||
// ListResult is one page of projected rows.
|
||||
@@ -139,21 +145,82 @@ func ExecuteList(ctx context.Context, db *gorm.DB, cc *CompiledController, in Li
|
||||
}
|
||||
values := slice.Elem()
|
||||
data := make([]map[string]any, 0, values.Len())
|
||||
records := make([]any, 0, values.Len())
|
||||
for i := 0; i < values.Len(); i++ {
|
||||
data = append(data, projectRow(values.Index(i).Addr().Interface(), cc.Controller, cc.List.Columns))
|
||||
record := values.Index(i).Addr().Interface()
|
||||
records = append(records, record)
|
||||
data = append(data, projectRow(record, cc.Controller, cc.List.Columns))
|
||||
}
|
||||
states, err := listRowStates(ctx, db, cc, records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paged := lagoon.Paginate(data, page, per, total)
|
||||
return &ListResult{
|
||||
Data: paged.Data,
|
||||
Meta: ListMeta{
|
||||
Page: paged.Meta.CurrentPage,
|
||||
PerPage: paged.Meta.PerPage,
|
||||
Total: paged.Meta.Total,
|
||||
LastPage: paged.Meta.LastPage,
|
||||
Page: paged.Meta.CurrentPage,
|
||||
PerPage: paged.Meta.PerPage,
|
||||
Total: paged.Meta.Total,
|
||||
LastPage: paged.Meta.LastPage,
|
||||
RowStates: states,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// rowStateOrder is the fixed set of row states and the order they are sent in.
|
||||
var rowStateOrder = []pact.RowState{pact.RowStateDeleted, pact.RowStateNegative, pact.RowStateDisabled}
|
||||
|
||||
// listRowStates asks a controller implementing pact.ListRowStates for the
|
||||
// states of one page of records, in one call (D-12). Each row keeps only the
|
||||
// known states, once each, in the fixed order; an unknown value is dropped
|
||||
// and logged, never sent. The result is keyed by primary key and nil when no
|
||||
// row has a state. A hook error or a result that is not index-aligned with
|
||||
// records fails the list.
|
||||
func listRowStates(ctx context.Context, db *gorm.DB, cc *CompiledController, records []any) (map[string][]string, error) {
|
||||
hook, ok := cc.Controller.(pact.ListRowStates)
|
||||
if !ok || hook == nil || len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
result, err := hook.ListRowStates(ctx, db.WithContext(ctx), records)
|
||||
if err != nil {
|
||||
slog.Error("cabana: list row states failed", "controller", controllerID(cc), "error", err)
|
||||
return nil, err
|
||||
}
|
||||
if len(result) != len(records) {
|
||||
slog.Error("cabana: list row states are not aligned with the page", "controller", controllerID(cc), "records", len(records), "states", len(result))
|
||||
return nil, fmt.Errorf("cabana: controller %s returned %d row states for %d records", controllerID(cc), len(result), len(records))
|
||||
}
|
||||
var out map[string][]string
|
||||
for i, states := range result {
|
||||
if len(states) == 0 {
|
||||
continue
|
||||
}
|
||||
has := map[pact.RowState]bool{}
|
||||
for _, state := range states {
|
||||
if !slices.Contains(rowStateOrder, state) {
|
||||
slog.Warn("cabana: unknown list row state dropped", "controller", controllerID(cc), "state", string(state))
|
||||
continue
|
||||
}
|
||||
has[state] = true
|
||||
}
|
||||
kept := make([]string, 0, len(has))
|
||||
for _, state := range rowStateOrder {
|
||||
if has[state] {
|
||||
kept = append(kept, string(state))
|
||||
}
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
continue
|
||||
}
|
||||
if out == nil {
|
||||
out = map[string][]string{}
|
||||
}
|
||||
out[uitoa(pkUint(records[i]))] = kept
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func normalizePage(schema *ListSchema, in ListInput) (int, int, error) {
|
||||
page := 1
|
||||
if in.Page != "" {
|
||||
|
||||
Reference in New Issue
Block a user