Keep each model's parent out of its JSON (closes #177)
check / check (push) Successful in 3m16s
check / check (push) Successful in 3m16s
Every reference from a model to the record it belongs to (a target's or entrypoint's webhook, a webhook's or API key's user, an event's webhook and entrypoint, a delivery's event and target, a delivery result's delivery) is now tagged json:"-", so a preloaded model can be marshalled without the encoder recursing between parent and child. References to child records stay. Tests marshal each of the nine references set, preloaded where it matters, and check the parent's id is absent, so deleting or restoring any one tag fails a test; preloading still fills each reference. Model: opus-5-5
This commit was merged in pull request #433.
This commit is contained in:
@@ -15,6 +15,7 @@ type APIKey struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
User User `json:"user,omitzero"`
|
// User.APIKeys leads back here, and the JSON could loop.
|
||||||
|
User User `json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,10 @@ type Delivery struct {
|
|||||||
// the index.
|
// the index.
|
||||||
FinishedAt *time.Time `gorm:"index:idx_deliveries_status,priority:3" json:"finishedAt,omitempty"`
|
FinishedAt *time.Time `gorm:"index:idx_deliveries_status,priority:3" json:"finishedAt,omitempty"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
Event Event `json:"event,omitzero"`
|
// Event.Deliveries and Target.Deliveries lead back here, and the
|
||||||
Target Target `json:"target,omitzero"`
|
// JSON could loop.
|
||||||
|
Event Event `json:"-"`
|
||||||
|
Target Target `json:"-"`
|
||||||
DeliveryResults []DeliveryResult `json:"deliveryResults,omitempty"`
|
DeliveryResults []DeliveryResult `json:"deliveryResults,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type DeliveryResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Duration int64 `json:"durationMs"` // Duration in milliseconds
|
Duration int64 `json:"durationMs"` // Duration in milliseconds
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
Delivery Delivery `json:"delivery,omitzero"`
|
// Delivery.DeliveryResults leads back here, and the JSON could loop.
|
||||||
|
Delivery Delivery `json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Entrypoint struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Active bool `gorm:"default:true" json:"active"`
|
Active bool `gorm:"default:true" json:"active"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
Webhook Webhook `json:"webhook,omitzero"`
|
// Webhook.Entrypoints leads back here, and the JSON could loop.
|
||||||
|
Webhook Webhook `json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ type Event struct {
|
|||||||
// kept as the record of where the copy came from either way.
|
// kept as the record of where the copy came from either way.
|
||||||
ResubmittedFromID *string `gorm:"type:uuid;index" json:"resubmittedFromId,omitempty"`
|
ResubmittedFromID *string `gorm:"type:uuid;index" json:"resubmittedFromId,omitempty"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to, so
|
||||||
Webhook Webhook `json:"webhook,omitzero"`
|
// Webhook and Entrypoint are left out of the JSON.
|
||||||
Entrypoint Entrypoint `json:"entrypoint,omitzero"`
|
Webhook Webhook `json:"-"`
|
||||||
|
Entrypoint Entrypoint `json:"-"`
|
||||||
Deliveries []Delivery `json:"deliveries,omitempty"`
|
Deliveries []Delivery `json:"deliveries,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package database_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPreloadedModelsMarshalWithoutTheirParent pins that a child's
|
||||||
|
// reference to the record it belongs to is left out of the JSON, so a
|
||||||
|
// webhook and its targets cannot marshal each other in a loop, and that
|
||||||
|
// GORM still preloads that reference, since it ignores json tags.
|
||||||
|
func TestPreloadedModelsMarshalWithoutTheirParent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
db := startedTestDB(t)
|
||||||
|
|
||||||
|
stored := database.Webhook{
|
||||||
|
UserID: uuid.New().String(),
|
||||||
|
Name: testWebhookName,
|
||||||
|
Entrypoints: []database.Entrypoint{{Path: uuid.New().String()}},
|
||||||
|
Targets: []database.Target{{
|
||||||
|
Name: "log",
|
||||||
|
Type: database.TargetTypeLog,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
require.NoError(t, db.Create(&stored).Error)
|
||||||
|
|
||||||
|
entrypointID := stored.Entrypoints[0].ID
|
||||||
|
targetID := stored.Targets[0].ID
|
||||||
|
|
||||||
|
var webhook database.Webhook
|
||||||
|
|
||||||
|
require.NoError(t, db.
|
||||||
|
Preload("Entrypoints.Webhook").
|
||||||
|
Preload("Targets.Webhook").
|
||||||
|
First(&webhook, "id = ?", stored.ID).Error)
|
||||||
|
|
||||||
|
require.Len(t, webhook.Entrypoints, 1)
|
||||||
|
require.Len(t, webhook.Targets, 1)
|
||||||
|
assert.Equal(t, stored.ID, webhook.Entrypoints[0].Webhook.ID)
|
||||||
|
assert.Equal(t, stored.ID, webhook.Targets[0].Webhook.ID)
|
||||||
|
|
||||||
|
encoded := marshalModel(t, webhook)
|
||||||
|
|
||||||
|
assert.Contains(t, encoded, entrypointID)
|
||||||
|
assert.Contains(t, encoded, targetID)
|
||||||
|
|
||||||
|
// Each child holds the parent's id as its webhookId, so the parent
|
||||||
|
// is looked for by its own id field.
|
||||||
|
parentIDField := `"id":"` + stored.ID + `"`
|
||||||
|
|
||||||
|
assert.NotContains(t, marshalModel(t, webhook.Entrypoints[0]), parentIDField)
|
||||||
|
assert.NotContains(t, marshalModel(t, webhook.Targets[0]), parentIDField)
|
||||||
|
|
||||||
|
var target database.Target
|
||||||
|
|
||||||
|
require.NoError(t, db.
|
||||||
|
Preload("Webhook").
|
||||||
|
First(&target, "id = ?", targetID).Error)
|
||||||
|
|
||||||
|
assert.Equal(t, stored.ID, target.Webhook.ID)
|
||||||
|
|
||||||
|
encoded = marshalModel(t, target)
|
||||||
|
|
||||||
|
assert.Contains(t, encoded, stored.ID)
|
||||||
|
assert.NotContains(t, encoded, parentIDField)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestModelsMarshalWithoutTheirParent covers the other references to a
|
||||||
|
// parent: each model is built with its parent set, and the parent's id
|
||||||
|
// must not appear in the JSON.
|
||||||
|
func TestModelsMarshalWithoutTheirParent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
parent := database.BaseModel{ID: uuid.New().String()}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
model any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Webhook.User",
|
||||||
|
model: database.Webhook{User: database.User{BaseModel: parent}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "APIKey.User",
|
||||||
|
model: database.APIKey{User: database.User{BaseModel: parent}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delivery.Event",
|
||||||
|
model: database.Delivery{Event: database.Event{BaseModel: parent}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delivery.Target",
|
||||||
|
model: database.Delivery{Target: database.Target{BaseModel: parent}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DeliveryResult.Delivery",
|
||||||
|
model: database.DeliveryResult{
|
||||||
|
Delivery: database.Delivery{BaseModel: parent},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Event.Webhook",
|
||||||
|
model: database.Event{Webhook: database.Webhook{BaseModel: parent}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Event.Entrypoint",
|
||||||
|
model: database.Event{
|
||||||
|
Entrypoint: database.Entrypoint{BaseModel: parent},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert.NotContains(t, marshalModel(t, tc.model), parent.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,8 @@ type Target struct {
|
|||||||
MaxRetries int `json:"maxRetries,omitempty"`
|
MaxRetries int `json:"maxRetries,omitempty"`
|
||||||
MaxQueueSize int `json:"maxQueueSize,omitempty"`
|
MaxQueueSize int `json:"maxQueueSize,omitempty"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
Webhook Webhook `json:"webhook,omitzero"`
|
// Webhook.Targets leads back here, and the JSON could loop.
|
||||||
|
Webhook Webhook `json:"-"`
|
||||||
Deliveries []Delivery `json:"deliveries,omitempty"`
|
Deliveries []Delivery `json:"deliveries,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ type Webhook struct {
|
|||||||
// must equal DefaultRetentionDays.
|
// must equal DefaultRetentionDays.
|
||||||
RetentionDays int `gorm:"default:30" json:"retentionDays"`
|
RetentionDays int `gorm:"default:30" json:"retentionDays"`
|
||||||
|
|
||||||
// Relations
|
// Relations. No model marshals the record it belongs to:
|
||||||
User User `json:"user,omitzero"`
|
// User.Webhooks leads back here, and the JSON could loop.
|
||||||
|
User User `json:"-"`
|
||||||
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
|
Entrypoints []Entrypoint `json:"entrypoints,omitempty"`
|
||||||
Targets []Target `json:"targets,omitempty"`
|
Targets []Target `json:"targets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user