Fixed linting issues

This commit is contained in:
Hein 2025-11-11 11:32:30 +02:00
parent 7b8216b71c
commit ecd7b31910
14 changed files with 104 additions and 65 deletions

View File

@ -1,5 +1,7 @@
# 📜 ResolveSpec 📜 # 📜 ResolveSpec 📜
![Tests](https://github.com/bitechdev/ResolveSpec/workflows/Tests/badge.svg)
ResolveSpec is a flexible and powerful REST API specification and implementation that provides GraphQL-like capabilities while maintaining REST simplicity. It offers **two complementary approaches**: ResolveSpec is a flexible and powerful REST API specification and implementation that provides GraphQL-like capabilities while maintaining REST simplicity. It offers **two complementary approaches**:
1. **ResolveSpec** - Body-based API with JSON request options 1. **ResolveSpec** - Body-based API with JSON request options

View File

@ -100,6 +100,10 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
b.schema, b.tableName = parseTableName(fullTableName) b.schema, b.tableName = parseTableName(fullTableName)
} }
if provider, ok := model.(common.TableAliasProvider); ok {
b.tableAlias = provider.TableAlias()
}
return b return b
} }

View File

@ -86,6 +86,10 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
g.schema, g.tableName = parseTableName(fullTableName) g.schema, g.tableName = parseTableName(fullTableName)
} }
if provider, ok := model.(common.TableAliasProvider); ok {
g.tableAlias = provider.TableAlias()
}
return g return g
} }
@ -267,11 +271,12 @@ func (g *GormInsertQuery) Returning(columns ...string) common.InsertQuery {
func (g *GormInsertQuery) Exec(ctx context.Context) (common.Result, error) { func (g *GormInsertQuery) Exec(ctx context.Context) (common.Result, error) {
var result *gorm.DB var result *gorm.DB
if g.model != nil { switch {
case g.model != nil:
result = g.db.WithContext(ctx).Create(g.model) result = g.db.WithContext(ctx).Create(g.model)
} else if g.values != nil { case g.values != nil:
result = g.db.WithContext(ctx).Create(g.values) result = g.db.WithContext(ctx).Create(g.values)
} else { default:
result = g.db.WithContext(ctx).Create(map[string]interface{}{}) result = g.db.WithContext(ctx).Create(map[string]interface{}{})
} }
return &GormResult{result: result}, result.Error return &GormResult{result: result}, result.Error

View File

@ -130,7 +130,7 @@ func (h *HTTPRequest) AllHeaders() map[string]string {
// HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter // HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter
type HTTPResponseWriter struct { type HTTPResponseWriter struct {
resp http.ResponseWriter resp http.ResponseWriter
w common.ResponseWriter w common.ResponseWriter //nolint:unused
status int status int
} }

View File

@ -131,6 +131,10 @@ type TableNameProvider interface {
TableName() string TableName() string
} }
type TableAliasProvider interface {
TableAlias() string
}
// PrimaryKeyNameProvider interface for models that provide primary key column names // PrimaryKeyNameProvider interface for models that provide primary key column names
type PrimaryKeyNameProvider interface { type PrimaryKeyNameProvider interface {
GetIDName() string GetIDName() string

View File

@ -49,12 +49,13 @@ func GetModelColumnDetail(record reflect.Value) []ModelFieldDetail {
fielddetail.DataType = fieldtype.Type.Name() fielddetail.DataType = fieldtype.Type.Name()
fielddetail.SQLName = fnFindKeyVal(gormdetail, "column:") fielddetail.SQLName = fnFindKeyVal(gormdetail, "column:")
fielddetail.SQLDataType = fnFindKeyVal(gormdetail, "type:") fielddetail.SQLDataType = fnFindKeyVal(gormdetail, "type:")
if strings.Index(strings.ToLower(gormdetail), "identity") > 0 || gormdetailLower := strings.ToLower(gormdetail)
strings.Index(strings.ToLower(gormdetail), "primary_key") > 0 { switch {
case strings.Index(gormdetailLower, "identity") > 0 || strings.Index(gormdetailLower, "primary_key") > 0:
fielddetail.SQLKey = "primary_key" fielddetail.SQLKey = "primary_key"
} else if strings.Contains(strings.ToLower(gormdetail), "unique") { case strings.Contains(gormdetailLower, "unique"):
fielddetail.SQLKey = "unique" fielddetail.SQLKey = "unique"
} else if strings.Contains(strings.ToLower(gormdetail), "uniqueindex") { case strings.Contains(gormdetailLower, "uniqueindex"):
fielddetail.SQLKey = "uniqueindex" fielddetail.SQLKey = "uniqueindex"
} }

View File

@ -481,11 +481,12 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
case map[string]interface{}: case map[string]interface{}:
// Determine the ID to use // Determine the ID to use
var targetID interface{} var targetID interface{}
if urlID != "" { switch {
case urlID != "":
targetID = urlID targetID = urlID
} else if reqID != nil { case reqID != nil:
targetID = reqID targetID = reqID
} else if updates["id"] != nil { case updates["id"] != nil:
targetID = updates["id"] targetID = updates["id"]
} }
@ -723,11 +724,12 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
var itemID interface{} var itemID interface{}
// Check if item is a string ID or object with id field // Check if item is a string ID or object with id field
if idStr, ok := item.(string); ok { switch v := item.(type) {
itemID = idStr case string:
} else if itemMap, ok := item.(map[string]interface{}); ok { itemID = v
itemID = itemMap["id"] case map[string]interface{}:
} else { itemID = v["id"]
default:
// Try to use the item directly as ID // Try to use the item directly as ID
itemID = item itemID = item
} }

View File

@ -781,11 +781,12 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
query := h.db.NewUpdate().Table(tableName).SetMap(dataMap) query := h.db.NewUpdate().Table(tableName).SetMap(dataMap)
// Apply ID filter // Apply ID filter
if id != "" { switch {
case id != "":
query = query.Where("id = ?", id) query = query.Where("id = ?", id)
} else if idPtr != nil { case idPtr != nil:
query = query.Where("id = ?", *idPtr) query = query.Where("id = ?", *idPtr)
} else { default:
h.sendError(w, http.StatusBadRequest, "missing_id", "ID is required for update", nil) h.sendError(w, http.StatusBadRequest, "missing_id", "ID is required for update", nil)
return return
} }
@ -902,11 +903,12 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
var itemID interface{} var itemID interface{}
// Check if item is a string ID or object with id field // Check if item is a string ID or object with id field
if idStr, ok := item.(string); ok { switch v := item.(type) {
itemID = idStr case string:
} else if itemMap, ok := item.(map[string]interface{}); ok { itemID = v
itemID = itemMap["id"] case map[string]interface{}:
} else { itemID = v["id"]
default:
itemID = item itemID = item
} }
@ -1330,7 +1332,9 @@ func (h *Handler) sendResponse(w common.ResponseWriter, data interface{}, metada
Metadata: metadata, Metadata: metadata,
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.WriteJSON(response) if err := w.WriteJSON(response); err != nil {
logger.Error("Failed to write JSON response: %v", err)
}
} }
// sendFormattedResponse sends response with formatting options // sendFormattedResponse sends response with formatting options
@ -1350,7 +1354,9 @@ func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{
case "simple": case "simple":
// Simple format: just return the data array // Simple format: just return the data array
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.WriteJSON(data) if err := w.WriteJSON(data); err != nil {
logger.Error("Failed to write JSON response: %v", err)
}
case "syncfusion": case "syncfusion":
// Syncfusion format: { result: data, count: total } // Syncfusion format: { result: data, count: total }
response := map[string]interface{}{ response := map[string]interface{}{
@ -1360,7 +1366,9 @@ func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{
response["count"] = metadata.Total response["count"] = metadata.Total
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.WriteJSON(response) if err := w.WriteJSON(response); err != nil {
logger.Error("Failed to write JSON response: %v", err)
}
default: default:
// Default/detail format: standard response with metadata // Default/detail format: standard response with metadata
response := common.Response{ response := common.Response{
@ -1369,7 +1377,9 @@ func (h *Handler) sendFormattedResponse(w common.ResponseWriter, data interface{
Metadata: metadata, Metadata: metadata,
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.WriteJSON(response) if err := w.WriteJSON(response); err != nil {
logger.Error("Failed to write JSON response: %v", err)
}
} }
} }
@ -1397,7 +1407,9 @@ func (h *Handler) sendError(w common.ResponseWriter, statusCode int, code, messa
}, },
} }
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
w.WriteJSON(response) if err := w.WriteJSON(response); err != nil {
logger.Error("Failed to write JSON error response: %v", err)
}
} }
// FetchRowNumber calculates the row number of a specific record based on sorting and filtering // FetchRowNumber calculates the row number of a specific record based on sorting and filtering

View File

@ -416,16 +416,17 @@ func (h *Handler) parseSorting(options *ExtendedRequestOptions, value string) {
direction := "ASC" direction := "ASC"
colName := field colName := field
if strings.HasPrefix(field, "-") { switch {
case strings.HasPrefix(field, "-"):
direction = "DESC" direction = "DESC"
colName = strings.TrimPrefix(field, "-") colName = strings.TrimPrefix(field, "-")
} else if strings.HasPrefix(field, "+") { case strings.HasPrefix(field, "+"):
direction = "ASC" direction = "ASC"
colName = strings.TrimPrefix(field, "+") colName = strings.TrimPrefix(field, "+")
} else if strings.HasSuffix(field, " desc") { case strings.HasSuffix(field, " desc"):
direction = "DESC" direction = "DESC"
colName = strings.TrimSuffix(field, "desc") colName = strings.TrimSuffix(field, "desc")
} else if strings.HasSuffix(field, " asc") { case strings.HasSuffix(field, " asc"):
direction = "ASC" direction = "ASC"
colName = strings.TrimSuffix(field, "asc") colName = strings.TrimSuffix(field, "asc")
} }

View File

@ -62,6 +62,7 @@ import (
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/router" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/router"
"github.com/bitechdev/ResolveSpec/pkg/logger"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/modelregistry"
) )
@ -252,5 +253,7 @@ func ExampleBunRouterWithBunDB(bunDB *bun.DB) {
r := routerAdapter.GetBunRouter() r := routerAdapter.GetBunRouter()
// Start server // Start server
http.ListenAndServe(":8080", r) if err := http.ListenAndServe(":8080", r); err != nil {
logger.Error("Server failed to start: %v", err)
}
} }

View File

@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
// DBM "github.com/bitechdev/GoCore/pkg/models"
) )
// This file provides example implementations of the required security callbacks. // This file provides example implementations of the required security callbacks.

View File

@ -27,9 +27,7 @@ func RegisterSecurityHooks(handler *restheadspec.Handler, securityList *Security
}) })
// Hook 4 (Optional): Audit logging // Hook 4 (Optional): Audit logging
handler.Hooks().Register(restheadspec.AfterRead, func(hookCtx *restheadspec.HookContext) error { handler.Hooks().Register(restheadspec.AfterRead, logDataAccess)
return logDataAccess(hookCtx)
})
} }
// loadSecurityRules loads security configuration for the user and entity // loadSecurityRules loads security configuration for the user and entity
@ -162,7 +160,7 @@ func applyColumnSecurity(hookCtx *restheadspec.HookContext, securityList *Securi
resultValue = resultValue.Elem() resultValue = resultValue.Elem()
} }
err, maskedResult := securityList.ApplyColumnSecurity(resultValue, modelType, userID, schema, tablename) maskedResult, err := securityList.ApplyColumnSecurity(resultValue, modelType, userID, schema, tablename)
if err != nil { if err != nil {
logger.Warn("Column security error: %v", err) logger.Warn("Column security error: %v", err)
// Don't fail the request, just log the issue // Don't fail the request, just log the issue

View File

@ -5,11 +5,14 @@ import (
"net/http" "net/http"
) )
// contextKey is a custom type for context keys to avoid collisions
type contextKey string
const ( const (
// Context keys for user information // Context keys for user information
UserIDKey = "user_id" UserIDKey contextKey = "user_id"
UserRolesKey = "user_roles" UserRolesKey contextKey = "user_roles"
UserTokenKey = "user_token" UserTokenKey contextKey = "user_token"
) )
// AuthMiddleware extracts user authentication from request and adds to context // AuthMiddleware extracts user authentication from request and adds to context

View File

@ -73,8 +73,9 @@ type SecurityList struct {
LoadColumnSecurityCallback LoadColumnSecurityFunc LoadColumnSecurityCallback LoadColumnSecurityFunc
LoadRowSecurityCallback LoadRowSecurityFunc LoadRowSecurityCallback LoadRowSecurityFunc
} }
type CONTEXT_KEY string
const SECURITY_CONTEXT_KEY = "SecurityList" const SECURITY_CONTEXT_KEY CONTEXT_KEY = "SecurityList"
var GlobalSecurity SecurityList var GlobalSecurity SecurityList
@ -105,22 +106,22 @@ func maskString(pString string, maskStart, maskEnd int, maskChar string, invert
} }
for index, char := range pString { for index, char := range pString {
if invert && index >= middleIndex-maskStart && index <= middleIndex { if invert && index >= middleIndex-maskStart && index <= middleIndex {
newStr = newStr + maskChar newStr += maskChar
continue continue
} }
if invert && index <= middleIndex+maskEnd && index >= middleIndex { if invert && index <= middleIndex+maskEnd && index >= middleIndex {
newStr = newStr + maskChar newStr += maskChar
continue continue
} }
if !invert && index <= maskStart { if !invert && index <= maskStart {
newStr = newStr + maskChar newStr += maskChar
continue continue
} }
if !invert && index >= strLen-1-maskEnd { if !invert && index >= strLen-1-maskEnd {
newStr = newStr + maskChar newStr += maskChar
continue continue
} }
newStr = newStr + string(char) newStr += string(char)
} }
return newStr return newStr
@ -145,7 +146,8 @@ func (m *SecurityList) ColumSecurityApplyOnRecord(prevRecord reflect.Value, newR
return cols, fmt.Errorf("no security data") return cols, fmt.Errorf("no security data")
} }
for _, colsec := range colsecList { for i := range colsecList {
colsec := &colsecList[i]
if !strings.EqualFold(colsec.Accesstype, "mask") && !strings.EqualFold(colsec.Accesstype, "hide") { if !strings.EqualFold(colsec.Accesstype, "mask") && !strings.EqualFold(colsec.Accesstype, "hide") {
continue continue
} }
@ -262,24 +264,25 @@ func setColSecValue(fieldsrc reflect.Value, colsec ColumnSecurity, fieldTypeName
fieldval = fieldval.Elem() fieldval = fieldval.Elem()
} }
if strings.Contains(strings.ToLower(fieldval.Kind().String()), "int") && fieldKindLower := strings.ToLower(fieldval.Kind().String())
(strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")) { switch {
case strings.Contains(fieldKindLower, "int") &&
(strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")):
if fieldval.CanInt() && fieldval.CanSet() { if fieldval.CanInt() && fieldval.CanSet() {
fieldval.SetInt(0) fieldval.SetInt(0)
} }
} else if (strings.Contains(strings.ToLower(fieldval.Kind().String()), "time") || case (strings.Contains(fieldKindLower, "time") || strings.Contains(fieldKindLower, "date")) &&
strings.Contains(strings.ToLower(fieldval.Kind().String()), "date")) && (strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")):
(strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")) {
fieldval.SetZero() fieldval.SetZero()
} else if strings.Contains(strings.ToLower(fieldval.Kind().String()), "string") { case strings.Contains(fieldKindLower, "string"):
strVal := fieldval.String() strVal := fieldval.String()
if strings.EqualFold(colsec.Accesstype, "mask") { if strings.EqualFold(colsec.Accesstype, "mask") {
fieldval.SetString(maskString(strVal, colsec.MaskStart, colsec.MaskEnd, colsec.MaskChar, colsec.MaskInvert)) fieldval.SetString(maskString(strVal, colsec.MaskStart, colsec.MaskEnd, colsec.MaskChar, colsec.MaskInvert))
} else if strings.EqualFold(colsec.Accesstype, "hide") { } else if strings.EqualFold(colsec.Accesstype, "hide") {
fieldval.SetString("") fieldval.SetString("")
} }
} else if strings.Contains(fieldTypeName, "json") && case strings.Contains(fieldTypeName, "json") &&
(strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")) { (strings.EqualFold(colsec.Accesstype, "mask") || strings.EqualFold(colsec.Accesstype, "hide")):
if len(colsec.Path) < 2 { if len(colsec.Path) < 2 {
return 1, fieldval return 1, fieldval
} }
@ -300,11 +303,11 @@ func setColSecValue(fieldsrc reflect.Value, colsec ColumnSecurity, fieldTypeName
return 0, fieldsrc return 0, fieldsrc
} }
func (m *SecurityList) ApplyColumnSecurity(records reflect.Value, modelType reflect.Type, pUserID int, pSchema, pTablename string) (error, reflect.Value) { func (m *SecurityList) ApplyColumnSecurity(records reflect.Value, modelType reflect.Type, pUserID int, pSchema, pTablename string) (reflect.Value, error) {
defer logger.CatchPanic("ApplyColumnSecurity") defer logger.CatchPanic("ApplyColumnSecurity")
if m.ColumnSecurity == nil { if m.ColumnSecurity == nil {
return fmt.Errorf("security not initialized"), records return records, fmt.Errorf("security not initialized")
} }
m.ColumnSecurityMutex.RLock() m.ColumnSecurityMutex.RLock()
@ -312,10 +315,11 @@ func (m *SecurityList) ApplyColumnSecurity(records reflect.Value, modelType refl
colsecList, ok := m.ColumnSecurity[fmt.Sprintf("%s.%s@%d", pSchema, pTablename, pUserID)] colsecList, ok := m.ColumnSecurity[fmt.Sprintf("%s.%s@%d", pSchema, pTablename, pUserID)]
if !ok || colsecList == nil { if !ok || colsecList == nil {
return fmt.Errorf("no security data"), records return records, fmt.Errorf("no security data")
} }
for _, colsec := range colsecList { for i := range colsecList {
colsec := &colsecList[i]
if !strings.EqualFold(colsec.Accesstype, "mask") && !strings.EqualFold(colsec.Accesstype, "hide") { if !strings.EqualFold(colsec.Accesstype, "mask") && !strings.EqualFold(colsec.Accesstype, "hide") {
continue continue
} }
@ -353,7 +357,7 @@ func (m *SecurityList) ApplyColumnSecurity(records reflect.Value, modelType refl
if i == pathLen-1 { if i == pathLen-1 {
if nameType == "sql" || nameType == "struct" { if nameType == "sql" || nameType == "struct" {
setColSecValue(field, colsec, fieldName) setColSecValue(field, *colsec, fieldName)
} }
break break
} }
@ -365,7 +369,7 @@ func (m *SecurityList) ApplyColumnSecurity(records reflect.Value, modelType refl
} }
} }
return nil, records return records, nil
} }
func (m *SecurityList) LoadColumnSecurity(pUserID int, pSchema, pTablename string, pOverwrite bool) error { func (m *SecurityList) LoadColumnSecurity(pUserID int, pSchema, pTablename string, pOverwrite bool) error {
@ -407,9 +411,10 @@ func (m *SecurityList) ClearSecurity(pUserID int, pSchema, pTablename string) er
return nil return nil
} }
for _, cs := range list { for i := range list {
cs := &list[i]
if cs.Schema != pSchema && cs.Tablename != pTablename && cs.UserID != pUserID { if cs.Schema != pSchema && cs.Tablename != pTablename && cs.UserID != pUserID {
filtered = append(filtered, cs) filtered = append(filtered, *cs)
} }
} }