mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 04:21:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20ba8ed112 |
@@ -0,0 +1,23 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// MergeUpdateValues merges the incoming request values into the existing
|
||||||
|
// record map (in place) and returns it.
|
||||||
|
//
|
||||||
|
// Every key present in incoming overwrites the existing value, including empty
|
||||||
|
// strings and explicit nulls, so clients can clear a column by sending "" or
|
||||||
|
// null. Keys absent from incoming are left untouched.
|
||||||
|
//
|
||||||
|
// When disallowNulls is true, nil values are skipped and the existing value is
|
||||||
|
// kept. Empty strings are still applied.
|
||||||
|
func MergeUpdateValues(existing, incoming map[string]interface{}, disallowNulls bool) map[string]interface{} {
|
||||||
|
if existing == nil {
|
||||||
|
existing = make(map[string]interface{}, len(incoming))
|
||||||
|
}
|
||||||
|
for key, newValue := range incoming {
|
||||||
|
if newValue == nil && disallowNulls {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing[key] = newValue
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestMergeUpdateValues(t *testing.T) {
|
||||||
|
newExisting := func() map[string]interface{} {
|
||||||
|
return map[string]interface{}{"name": "old", "note": "keep", "other": "x"}
|
||||||
|
}
|
||||||
|
incoming := map[string]interface{}{"name": "", "note": nil}
|
||||||
|
|
||||||
|
got := MergeUpdateValues(newExisting(), incoming, false)
|
||||||
|
if got["name"] != "" {
|
||||||
|
t.Errorf("name = %v, want empty string", got["name"])
|
||||||
|
}
|
||||||
|
if v, ok := got["note"]; !ok || v != nil {
|
||||||
|
t.Errorf("note = %v (present=%v), want nil", v, ok)
|
||||||
|
}
|
||||||
|
if got["other"] != "x" {
|
||||||
|
t.Errorf("absent key changed: %v", got["other"])
|
||||||
|
}
|
||||||
|
|
||||||
|
got = MergeUpdateValues(newExisting(), incoming, true)
|
||||||
|
if got["name"] != "" {
|
||||||
|
t.Errorf("disallowNulls: name = %v, want empty string", got["name"])
|
||||||
|
}
|
||||||
|
if got["note"] != "keep" {
|
||||||
|
t.Errorf("disallowNulls: note = %v, want keep", got["note"])
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
-15
@@ -45,6 +45,9 @@ type Handler struct {
|
|||||||
// Started flag
|
// Started flag
|
||||||
started bool
|
started bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// disallowNulls skips null values in update payloads instead of applying them
|
||||||
|
disallowNulls bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new MQTT handler
|
// NewHandler creates a new MQTT handler
|
||||||
@@ -812,6 +815,14 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create creates a new record
|
// create creates a new record
|
||||||
|
// SetDisallowNulls controls whether explicit null values in update payloads are
|
||||||
|
// ignored. By default a key present in the payload overwrites the stored value,
|
||||||
|
// including "" and null. When true, null values are skipped and the existing
|
||||||
|
// value is kept ("" still overwrites).
|
||||||
|
func (h *Handler) SetDisallowNulls(disallow bool) {
|
||||||
|
h.disallowNulls = disallow
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
||||||
// Marshal and unmarshal data into model
|
// Marshal and unmarshal data into model
|
||||||
dataBytes, err := json.Marshal(hookCtx.Data)
|
dataBytes, err := json.Marshal(hookCtx.Data)
|
||||||
@@ -840,25 +851,33 @@ func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
|||||||
|
|
||||||
// update updates an existing record
|
// update updates an existing record
|
||||||
func (h *Handler) update(hookCtx *HookContext) (interface{}, error) {
|
func (h *Handler) update(hookCtx *HookContext) (interface{}, error) {
|
||||||
// Marshal and unmarshal data into model
|
// Convert request data to a map
|
||||||
dataBytes, err := json.Marshal(hookCtx.Data)
|
var updates map[string]interface{}
|
||||||
if err != nil {
|
if m, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
return nil, fmt.Errorf("failed to marshal data: %w", err)
|
updates = m
|
||||||
|
} else {
|
||||||
|
dataBytes, err := json.Marshal(hookCtx.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal data: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(dataBytes, &updates); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal data into map: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(dataBytes, hookCtx.ModelPtr); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal data into model: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update record
|
|
||||||
query := h.db.NewUpdate().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
|
||||||
|
|
||||||
// Add ID filter
|
|
||||||
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
|
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", pkName), hookCtx.ID)
|
|
||||||
|
|
||||||
if _, err := query.Exec(hookCtx.Context); err != nil {
|
// Only the keys present in the request are written. "" and null overwrite
|
||||||
return nil, fmt.Errorf("failed to update record: %w", err)
|
// the stored value unless disallowNulls is set, in which case null is skipped.
|
||||||
|
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
||||||
|
|
||||||
|
if len(values) > 0 {
|
||||||
|
query := h.db.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
||||||
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
||||||
|
|
||||||
|
if _, err := query.Exec(hookCtx.Context); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update record: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch updated record
|
// Fetch updated record
|
||||||
|
|||||||
+15
-35
@@ -32,6 +32,7 @@ type Handler struct {
|
|||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
defaultSort map[string][]common.SortOption
|
defaultSort map[string][]common.SortOption
|
||||||
|
disallowNulls bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -52,6 +53,14 @@ func (h *Handler) Hooks() *HookRegistry {
|
|||||||
return h.hooks
|
return h.hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisallowNulls controls whether explicit null values in update payloads are
|
||||||
|
// ignored. By default a key present in the payload overwrites the stored value,
|
||||||
|
// including "" and null. When true, null values are skipped and the existing
|
||||||
|
// value is kept ("" still overwrites).
|
||||||
|
func (h *Handler) SetDisallowNulls(disallow bool) {
|
||||||
|
h.disallowNulls = disallow
|
||||||
|
}
|
||||||
|
|
||||||
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||||
// If not set, the handler will simply return (pass through to next route)
|
// If not set, the handler will simply return (pass through to next route)
|
||||||
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||||
@@ -1236,21 +1245,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
return fmt.Errorf("error unmarshaling existing record: %w", err)
|
return fmt.Errorf("error unmarshaling existing record: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge only non-null and non-empty values from the incoming request into the existing record
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
for key, newValue := range updates {
|
common.MergeUpdateValues(existingMap, updates, h.disallowNulls)
|
||||||
// Skip if the value is nil
|
|
||||||
if newValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if the value is an empty string
|
|
||||||
if strVal, ok := newValue.(string); ok && strVal == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the existing map with the new value
|
|
||||||
existingMap[key] = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build update query with merged data
|
// Build update query with merged data
|
||||||
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
||||||
@@ -1421,16 +1417,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
item = modifiedData
|
item = modifiedData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge only non-null and non-empty values
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
for key, newValue := range item {
|
common.MergeUpdateValues(existingMap, item, h.disallowNulls)
|
||||||
if newValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strVal, ok := newValue.(string); ok && strVal == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
existingMap[key] = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
@@ -1578,16 +1566,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
itemMap = modifiedData
|
itemMap = modifiedData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge only non-null and non-empty values
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
for key, newValue := range itemMap {
|
common.MergeUpdateValues(existingMap, itemMap, h.disallowNulls)
|
||||||
if newValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strVal, ok := newValue.(string); ok && strVal == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
existingMap[key] = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
|
|||||||
+11
-15
@@ -33,6 +33,7 @@ type Handler struct {
|
|||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
defaultSort map[string][]common.SortOption
|
defaultSort map[string][]common.SortOption
|
||||||
|
disallowNulls bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -59,6 +60,14 @@ func (h *Handler) Hooks() *HookRegistry {
|
|||||||
return h.hooks
|
return h.hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisallowNulls controls whether explicit null values in update payloads are
|
||||||
|
// ignored. By default a key present in the payload overwrites the stored value,
|
||||||
|
// including "" and null. When true, null values are skipped and the existing
|
||||||
|
// value is kept ("" still overwrites).
|
||||||
|
func (h *Handler) SetDisallowNulls(disallow bool) {
|
||||||
|
h.disallowNulls = disallow
|
||||||
|
}
|
||||||
|
|
||||||
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||||
// If not set, the handler will simply return (pass through to next route)
|
// If not set, the handler will simply return (pass through to next route)
|
||||||
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||||
@@ -1597,21 +1606,8 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
nestedRelations = relations
|
nestedRelations = relations
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge only non-null and non-empty values from the incoming request into the existing record
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
for key, newValue := range dataMap {
|
common.MergeUpdateValues(existingMap, dataMap, h.disallowNulls)
|
||||||
// Skip if the value is nil
|
|
||||||
if newValue == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if the value is an empty string
|
|
||||||
if strVal, ok := newValue.(string); ok && strVal == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the existing map with the new value
|
|
||||||
existingMap[key] = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure ID is in the data map for the update
|
// Ensure ID is in the data map for the update
|
||||||
existingMap[pkName] = targetID
|
existingMap[pkName] = targetID
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ type Handler struct {
|
|||||||
subscriptionManager *SubscriptionManager
|
subscriptionManager *SubscriptionManager
|
||||||
upgrader websocket.Upgrader
|
upgrader websocket.Upgrader
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
|
// disallowNulls skips null values in update payloads instead of applying them
|
||||||
|
disallowNulls bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new WebSocket handler
|
// NewHandler creates a new WebSocket handler
|
||||||
@@ -682,6 +685,14 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
return hookCtx.ModelPtr, metadata, nil
|
return hookCtx.ModelPtr, metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDisallowNulls controls whether explicit null values in update payloads are
|
||||||
|
// ignored. By default a key present in the payload overwrites the stored value,
|
||||||
|
// including "" and null. When true, null values are skipped and the existing
|
||||||
|
// value is kept ("" still overwrites).
|
||||||
|
func (h *Handler) SetDisallowNulls(disallow bool) {
|
||||||
|
h.disallowNulls = disallow
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
||||||
// Marshal and unmarshal data into model
|
// Marshal and unmarshal data into model
|
||||||
dataBytes, err := json.Marshal(hookCtx.Data)
|
dataBytes, err := json.Marshal(hookCtx.Data)
|
||||||
@@ -709,25 +720,33 @@ func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) update(hookCtx *HookContext) (interface{}, error) {
|
func (h *Handler) update(hookCtx *HookContext) (interface{}, error) {
|
||||||
// Marshal and unmarshal data into model
|
// Convert request data to a map
|
||||||
dataBytes, err := json.Marshal(hookCtx.Data)
|
var updates map[string]interface{}
|
||||||
if err != nil {
|
if m, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
return nil, fmt.Errorf("failed to marshal data: %w", err)
|
updates = m
|
||||||
|
} else {
|
||||||
|
dataBytes, err := json.Marshal(hookCtx.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal data: %w", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(dataBytes, &updates); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal data into map: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(dataBytes, hookCtx.ModelPtr); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal data into model: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update record
|
|
||||||
query := h.db.NewUpdate().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
|
||||||
|
|
||||||
// Add ID filter
|
|
||||||
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
|
pkName := reflection.GetPrimaryKeyName(hookCtx.Model)
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", pkName), hookCtx.ID)
|
|
||||||
|
|
||||||
if _, err := query.Exec(hookCtx.Context); err != nil {
|
// Only the keys present in the request are written. "" and null overwrite
|
||||||
return nil, fmt.Errorf("failed to update record: %w", err)
|
// the stored value unless disallowNulls is set, in which case null is skipped.
|
||||||
|
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
||||||
|
|
||||||
|
if len(values) > 0 {
|
||||||
|
query := h.db.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
||||||
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
||||||
|
|
||||||
|
if _, err := query.Exec(hookCtx.Context); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update record: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch updated record
|
// Fetch updated record
|
||||||
|
|||||||
Reference in New Issue
Block a user