Every reference from a model to the record it belongs to is now json:"-", so the webhook and target models (and the other parent and child pairs in internal/database) can no longer marshal each other in a loop. Preloading maps associations without reading json tags, so it still fills these references. One test preloads a webhook with its targets and entrypoints and a target with its webhook, checks the references are filled, and checks the JSON leaves them out; another builds each other model with its parent set and checks the parent's id is absent from its JSON. Model: opus-5-5
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