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:
@@ -125,6 +125,8 @@ func (s *service) mount(r pact.Router) {
|
||||
g.Get("/auth/me", s.me)
|
||||
g.Get("/{vendor}/{plugin}/{controller}/schema/list", s.listSchema)
|
||||
constrainController(g)
|
||||
g.Get("/{vendor}/{plugin}/{controller}/schema/form", s.formSchema)
|
||||
constrainController(g)
|
||||
g.Get("/{vendor}/{plugin}/{controller}", s.list)
|
||||
constrainController(g)
|
||||
g.Post("/{vendor}/{plugin}/{controller}", s.create)
|
||||
@@ -146,6 +148,25 @@ func constrainController(g pact.Router) {
|
||||
g.Where("controller", "[A-Za-z0-9_-]+")
|
||||
}
|
||||
|
||||
func (s *service) formSchema(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
if cc.Form == nil {
|
||||
WriteError(w, http.StatusNotFound, "not_found", msgNotFound)
|
||||
return
|
||||
}
|
||||
view, err := cc.Form.Localize(r.Context(), s.translator(), dropdownProvider(cc.Controller))
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||
return
|
||||
}
|
||||
meta := map[string]any{}
|
||||
if view.Meta.Locale != "" {
|
||||
meta["locale"] = view.Meta.Locale
|
||||
}
|
||||
WriteData(w, http.StatusOK, view, meta)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) listSchema(w http.ResponseWriter, r *http.Request) {
|
||||
s.protect(w, r, func(cc *CompiledController) {
|
||||
schema := cc.List
|
||||
|
||||
@@ -330,6 +330,9 @@ func listModelContract(ctl pact.AdminController) (map[string]struct{}, map[strin
|
||||
}
|
||||
if isListRelation(field.Type) {
|
||||
rels[field.Name] = struct{}{}
|
||||
// Winter YAML spells relations in lowercase (relation: genre)
|
||||
// while the Go field is exported (Genre).
|
||||
rels[strings.ToLower(field.Name)] = struct{}{}
|
||||
continue
|
||||
}
|
||||
name := gormColumn(field)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user