Enhance OpenAPI and JSON Parsing Functionality

- Added app handlers to the OpenAPI attachment process, improving routing for application-related endpoints.
- Refactored the parseJSONField function to handle both JSON and boolean column types, enhancing value parsing flexibility and robustness.
- Improved error handling and clarity in the JSON parsing logic, ensuring original values are returned when parsing fails.
This commit is contained in:
Max 2025-12-31 11:54:36 +08:00
parent 4d1b427634
commit d3ed830ec1
4 changed files with 128 additions and 16 deletions

91
openapi/app/app.go Normal file
View file

@ -0,0 +1,91 @@
package app
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Attach attaches the app handlers to the router
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
// Menu endpoint - requires authentication
group.GET("/menu", oauth.Guard, getMenu)
}
// MenuRequest represents the menu request parameters
type MenuRequest struct {
Locale string `form:"locale" json:"locale"`
}
// getMenu handles GET /app/menu
// Returns the application menu based on user permissions and locale
func getMenu(c *gin.Context) {
var req MenuRequest
if err := c.ShouldBindQuery(&req); err != nil {
response.RespondWithError(c, http.StatusBadRequest, &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: err.Error(),
})
return
}
// Get authorized info from context (set by oauth.Guard)
authInfo := authorized.GetInfo(c)
if authInfo == nil {
response.RespondWithError(c, http.StatusUnauthorized, &response.ErrorResponse{
Code: response.ErrInvalidToken.Code,
ErrorDescription: "Authorization required",
})
return
}
// Call yao.app.Menu process with locale parameter
handle, err := process.Of("yao.app.Menu", req.Locale)
if err != nil {
response.RespondWithError(c, http.StatusBadRequest, &response.ErrorResponse{
Code: response.ErrInvalidRequest.Code,
ErrorDescription: err.Error(),
})
return
}
// Set process context
handle.WithSID(authInfo.SessionID)
// Set authorized info for process
handle.WithAuthorized(map[string]interface{}{
"subject": authInfo.Subject,
"client_id": authInfo.ClientID,
"user_id": authInfo.UserID,
"scope": authInfo.Scope,
"team_id": authInfo.TeamID,
"tenant_id": authInfo.TenantID,
"session_id": authInfo.SessionID,
"remember_me": authInfo.RememberMe,
"constraints": map[string]interface{}{
"owner_only": authInfo.Constraints.OwnerOnly,
"creator_only": authInfo.Constraints.CreatorOnly,
"editor_only": authInfo.Constraints.EditorOnly,
"team_only": authInfo.Constraints.TeamOnly,
"extra": authInfo.Constraints.Extra,
},
})
// Execute the process
err = handle.Execute()
if err != nil {
response.RespondWithError(c, http.StatusInternalServerError, &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: err.Error(),
})
return
}
defer handle.Dispose()
// Return the menu data
response.RespondWithSuccess(c, http.StatusOK, handle.Value())
}

View file

@ -7,6 +7,7 @@ import (
"github.com/yaoapp/gou/application"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/agent"
"github.com/yaoapp/yao/openapi/app"
"github.com/yaoapp/yao/openapi/captcha"
"github.com/yaoapp/yao/openapi/chat"
"github.com/yaoapp/yao/openapi/dsl"
@ -150,6 +151,9 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) {
// Trace handlers
openapiTrace.Attach(group.Group("/trace"), openapi.OAuth)
// App handlers (menu, etc.)
app.Attach(group.Group("/app"), openapi.OAuth)
// Custom handlers (Defined by developer)
}

View file

@ -572,17 +572,14 @@ func buildColumnTypeMap(mod *model.Model, header []string) []string {
return columnTypes
}
// parseJSONField attempts to parse a value as JSON if the column type is json
// Returns the parsed JSON object if successful, otherwise returns the original value
// parseJSONField attempts to parse a value based on column type
// For JSON columns: parses JSON string to object
// For boolean columns: converts "true"/"false"/"1"/"0" to bool
// Returns the parsed value if successful, otherwise returns the original value
func parseJSONField(value interface{}, columnType string) interface{} {
// Check if column type is JSON
if columnType != "json" && columnType != "jsonb" {
return value
}
// Try to parse string value as JSON
// Try to parse string value
strValue, ok := value.(string)
if !ok || strValue == "" {
if !ok {
return value
}
@ -592,6 +589,19 @@ func parseJSONField(value interface{}, columnType string) interface{} {
return value
}
// Handle boolean type
if columnType == "boolean" || columnType == "bool" {
switch strings.ToLower(strValue) {
case "true", "1", "yes":
return true
case "false", "0", "no":
return false
}
return value
}
// Handle JSON type
if columnType == "json" || columnType == "jsonb" {
// Try to parse as JSON
var jsonValue interface{}
if err := json.Unmarshal([]byte(strValue), &jsonValue); err != nil {
@ -599,8 +609,10 @@ func parseJSONField(value interface{}, columnType string) interface{} {
// Don't log error as this is expected for non-JSON strings
return value
}
return jsonValue
}
return value
}
// sortColumns sorts column names alphabetically for consistent ordering

View file

@ -396,7 +396,12 @@ func processMenu(p *process.Process) interface{} {
exception.New(err.Error(), 400).Throw()
}
err = handle.WithGlobal(p.Global).WithSID(p.Sid).Execute()
handle.WithGlobal(p.Global).WithSID(p.Sid)
if p.Authorized != nil {
handle = handle.WithAuthorized(p.Authorized)
}
err = handle.Execute()
if err != nil {
exception.New(err.Error(), 500).Throw()
}