Files
summercms/cabana/schema_types.go
Jakub Zych fe04dbc89e feat(10-02): relation field options and relation saves with labels
- FieldRelationContract/FieldRelationProvider bind every type: relation
  field to a belongsTo foreign key or a belongsToMany pivot; activation
  fails naming plugin, controller and field on a missing or broken contract
- GET /{vendor}/{plugin}/{controller}/fields/{field}/options serves
  {value, label} pages scoped by pact.RelationExtendOptionsQuery, behind
  the controller permission; read-only and non-relation fields are 404
- Saves apply present relation keys after the Before hook: ids are
  revalidated through the same scoped query (422 and full rollback
  otherwise), belongsTo sets the foreign key, belongsToMany replaces pivot
  rows in submitted order with the order column set to the index
- Show, create and update return relation values in data and meta.labels
- A belongsTo on a protected fill key is read-only (D-26)
- One six-segment GET pattern dispatches relation lists and field options,
  which ServeMux cannot register side by side
- Admin OpenAPI documents the options route and RecordEnvelope
2026-09-27 16:00:52 +02:00

181 lines
6.0 KiB
Go

package cabana
import "encoding/json"
// ListSort is the compiled default sort. Direction is exactly asc or desc.
type ListSort struct {
Column string `json:"column"`
Direction string `json:"direction"`
}
// ListColumn is one compiled columns.yaml entry, in file order.
// Omitted sortable defaults to true, matching Winter; searchable defaults to false.
type ListColumn struct {
Key string `json:"key"`
Label string `json:"label"`
Searchable bool `json:"searchable"`
Sortable bool `json:"sortable"`
Type string `json:"type,omitempty"`
Relation string `json:"relation,omitempty"`
Select string `json:"select,omitempty"`
}
// ListFilter is one compiled config_filter scope. Values stay typed scalars.
type ListFilter struct {
Name string `json:"name"`
Label string `json:"label,omitempty"`
Type string `json:"type"`
Column string `json:"column,omitempty"`
Scope string `json:"scope,omitempty"`
ModelClass string `json:"modelClass,omitempty"`
NameFrom string `json:"nameFrom,omitempty"`
TrueValue *jsonScalar `json:"trueValue,omitempty"`
FalseValue *jsonScalar `json:"falseValue,omitempty"`
Options []FormOption `json:"options,omitempty"`
}
// RowAction is a per-record link derived from recordUrl.
type RowAction struct {
Name string `json:"name"`
Label string `json:"label,omitempty"`
URL string `json:"url,omitempty"`
}
// BulkAction is a checkbox action. It carries no SQL.
type BulkAction struct {
Name string `json:"name"`
Label string `json:"label,omitempty"`
}
// ListSchema is the boot-compiled list contract for one controller.
// Labels stay as source keys until a request localizes a copy.
type ListSchema struct {
Title string `json:"title,omitempty"`
ModelClass string `json:"modelClass,omitempty"`
RecordURL string `json:"recordUrl,omitempty"`
NoRecordsMessage string `json:"noRecordsMessage,omitempty"`
RecordsPerPage int `json:"recordsPerPage"`
PerPageOptions []int `json:"perPageOptions"`
ShowSearch bool `json:"showSearch"`
ShowSetup bool `json:"showSetup"`
ShowCheckboxes bool `json:"showCheckboxes"`
ShowSorting bool `json:"showSorting"`
SearchTerm string `json:"searchTerm"`
SearchPrompt string `json:"searchPrompt,omitempty"`
DefaultSort *ListSort `json:"defaultSort,omitempty"`
ToolbarButtons []string `json:"toolbarButtons"`
Columns []ListColumn `json:"columns"`
Filters []ListFilter `json:"filters"`
RowActions []RowAction `json:"rowActions"`
BulkActions []BulkAction `json:"bulkActions"`
Meta *FormMeta `json:"meta,omitempty"`
}
// MarshalJSON keeps omitted collections as arrays so a partial copy cannot emit null.
func (s ListSchema) MarshalJSON() ([]byte, error) {
type alias ListSchema
out := alias(s)
if out.PerPageOptions == nil {
out.PerPageOptions = []int{}
}
if out.ToolbarButtons == nil {
out.ToolbarButtons = []string{}
}
if out.Columns == nil {
out.Columns = []ListColumn{}
}
if out.Filters == nil {
out.Filters = []ListFilter{}
}
if out.RowActions == nil {
out.RowActions = []RowAction{}
}
if out.BulkActions == nil {
out.BulkActions = []BulkAction{}
}
return json.Marshal(out)
}
// FormSchema is the locale-neutral form contract compiled once at boot.
// Display strings stay as source keys until a request localizes a copy.
type FormSchema struct {
Name string `json:"name,omitempty"`
ModelClass string `json:"modelClass,omitempty"`
Fields []FormField `json:"fields"`
}
// FormView is one request's localized form, including the locale actually used.
type FormView struct {
Name string `json:"name,omitempty"`
ModelClass string `json:"modelClass,omitempty"`
Fields []FormField `json:"fields"`
Meta FormMeta `json:"meta"`
}
// FormMeta reports the locale selected for a schema response.
type FormMeta struct {
Locale string `json:"locale"`
}
// FormField is one Winter field in source order. JSON keys keep Winter spelling.
type FormField struct {
Name string `json:"name"`
Type string `json:"type"`
Label string `json:"label,omitempty"`
Comment string `json:"comment,omitempty"`
Span string `json:"span,omitempty"`
Tab string `json:"tab,omitempty"`
Size string `json:"size,omitempty"`
Context *fieldContext `json:"context,omitempty"`
NameFrom string `json:"nameFrom,omitempty"`
EmptyOption string `json:"emptyOption,omitempty"`
Relation string `json:"relation,omitempty"`
Multiple bool `json:"multiple,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Required bool `json:"required,omitempty"`
Default *jsonScalar `json:"default,omitempty"`
Attributes map[string]jsonScalar `json:"attributes,omitempty"`
Options []FormOption `json:"options,omitempty"`
optionsMethod string
}
// FormOption is one dropdown choice. Value keeps the YAML scalar's JSON type.
type FormOption struct {
Value jsonScalar `json:"value"`
Label string `json:"label"`
}
// jsonScalar is a JSON scalar that still emits false, 0, and empty string.
type jsonScalar struct {
raw json.RawMessage
}
func (s jsonScalar) MarshalJSON() ([]byte, error) {
if len(s.raw) == 0 {
return []byte("null"), nil
}
return s.raw, nil
}
// fieldContext preserves a single Winter context string or a source-ordered list.
type fieldContext struct {
single bool
values []string
}
func (c fieldContext) MarshalJSON() ([]byte, error) {
if c.single {
text := ""
if len(c.values) > 0 {
text = c.values[0]
}
return json.Marshal(text)
}
values := c.values
if values == nil {
values = []string{}
}
return json.Marshal(values)
}