feat(09-06): serve admin form schemas and match Winter relations

- GET schema/form localizes the compiled form after the permission check
- relation: genre resolves to the exported Go field without changing the YAML key
This commit is contained in:
Jakub Zych
2026-09-24 20:10:48 +02:00
parent 3e7738ff32
commit c63146accb
3 changed files with 62 additions and 29 deletions

View File

@@ -125,8 +125,11 @@ func ExecuteList(ctx context.Context, db *gorm.DB, cc *CompiledController, in Li
}
q = applyListOrder(q, cc.List, model, sortCol, sortDesc)
for _, col := range cc.List.Columns {
if col.Relation != "" {
q = q.Preload(col.Relation)
if col.Relation == "" {
continue
}
if field, ok := relationFieldName(model, col.Relation); ok {
q = q.Preload(field)
}
}
offset := (page - 1) * per
@@ -230,9 +233,13 @@ func applyListOrder(db *gorm.DB, schema *ListSchema, model any, column string, d
if col.Key != column || col.Relation == "" {
continue
}
orderTable = col.Relation
field, ok := relationFieldName(model, col.Relation)
if !ok {
continue
}
orderTable = field
orderColumn = col.Select
db = db.Joins(col.Relation)
db = db.Joins(field)
break
}
db = db.Order(clause.OrderByColumn{
@@ -269,15 +276,16 @@ func applyListSearch(db *gorm.DB, schema *ListSchema, model any, term string) (*
table := main
column := col.Key
if col.Relation != "" {
if _, ok := relatedTable(model, col.Relation); !ok || !identifier(col.Select) {
field, ok := relationFieldName(model, col.Relation)
if !ok || !identifier(col.Select) {
return nil, listInvalid("search", "is not searchable")
}
// GORM aliases the joined association with the relation field name.
table = col.Relation
// GORM aliases the joined association with the Go field name.
table = field
column = col.Select
if _, done := joined[col.Relation]; !done {
db = db.Joins(col.Relation)
joined[col.Relation] = struct{}{}
if _, done := joined[field]; !done {
db = db.Joins(field)
joined[field] = struct{}{}
}
} else if !identifier(column) {
return nil, listInvalid("search", "is not searchable")
@@ -439,33 +447,30 @@ func tableName(model any) string {
return ""
}
func relatedTable(model any, relation string) (string, bool) {
// relationFieldName resolves a Winter relation key onto the exported Go field.
// Exact names win; otherwise the match is case-insensitive, so relation: genre
// selects field Genre.
func relationFieldName(model any, relation string) (string, bool) {
t := reflect.TypeOf(model)
for t != nil && t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t == nil || t.Kind() != reflect.Struct {
if t == nil || t.Kind() != reflect.Struct || relation == "" {
return "", false
}
field, ok := t.FieldByName(relation)
if !ok {
return "", false
if field, ok := t.FieldByName(relation); ok && isListRelation(field.Type) {
return field.Name, true
}
rt := field.Type
for rt.Kind() == reflect.Pointer {
rt = rt.Elem()
}
if rt.Kind() == reflect.Slice {
rt = rt.Elem()
for rt.Kind() == reflect.Pointer {
rt = rt.Elem()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" || !isListRelation(field.Type) {
continue
}
if strings.EqualFold(field.Name, relation) {
return field.Name, true
}
}
if rt.Kind() != reflect.Struct {
return "", false
}
name := tableName(reflect.New(rt).Interface())
return name, name != ""
return "", false
}
func primaryColumn(model any) string {
@@ -501,7 +506,11 @@ func relatedSelect(row reflect.Value, col ListColumn) (any, bool) {
if row.Kind() != reflect.Struct || col.Relation == "" {
return nil, false
}
field := row.FieldByName(col.Relation)
name, ok := relationFieldName(row.Interface(), col.Relation)
if !ok {
return nil, false
}
field := row.FieldByName(name)
if !field.IsValid() {
return nil, false
}