diff --git a/internal/database/model_apikey.go b/internal/database/model_apikey.go index 5e8cee7..e39b101 100644 --- a/internal/database/model_apikey.go +++ b/internal/database/model_apikey.go @@ -15,6 +15,7 @@ type APIKey struct { Description string `json:"description"` LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` - // Relations - User User `json:"user,omitzero"` + // Relations. No model marshals the record it belongs to: + // User.APIKeys leads back here, and the JSON could loop. + User User `json:"-"` } diff --git a/internal/database/model_delivery.go b/internal/database/model_delivery.go index 4424756..ffaba8a 100644 --- a/internal/database/model_delivery.go +++ b/internal/database/model_delivery.go @@ -56,8 +56,10 @@ type Delivery struct { // the index. FinishedAt *time.Time `gorm:"index:idx_deliveries_status,priority:3" json:"finishedAt,omitempty"` - // Relations - Event Event `json:"event,omitzero"` - Target Target `json:"target,omitzero"` + // Relations. No model marshals the record it belongs to: + // Event.Deliveries and Target.Deliveries lead back here, and the + // JSON could loop. + Event Event `json:"-"` + Target Target `json:"-"` DeliveryResults []DeliveryResult `json:"deliveryResults,omitempty"` } diff --git a/internal/database/model_delivery_result.go b/internal/database/model_delivery_result.go index 7bf1b88..e293320 100644 --- a/internal/database/model_delivery_result.go +++ b/internal/database/model_delivery_result.go @@ -23,6 +23,7 @@ type DeliveryResult struct { Error string `json:"error,omitempty"` Duration int64 `json:"durationMs"` // Duration in milliseconds - // Relations - Delivery Delivery `json:"delivery,omitzero"` + // Relations. No model marshals the record it belongs to: + // Delivery.DeliveryResults leads back here, and the JSON could loop. + Delivery Delivery `json:"-"` } diff --git a/internal/database/model_entrypoint.go b/internal/database/model_entrypoint.go index 3021f48..ce8e42c 100644 --- a/internal/database/model_entrypoint.go +++ b/internal/database/model_entrypoint.go @@ -15,6 +15,7 @@ type Entrypoint struct { Description string `json:"description"` Active bool `gorm:"default:true" json:"active"` - // Relations - Webhook Webhook `json:"webhook,omitzero"` + // Relations. No model marshals the record it belongs to: + // Webhook.Entrypoints leads back here, and the JSON could loop. + Webhook Webhook `json:"-"` } diff --git a/internal/database/model_event.go b/internal/database/model_event.go index eba770b..8980a53 100644 --- a/internal/database/model_event.go +++ b/internal/database/model_event.go @@ -44,8 +44,9 @@ type Event struct { // kept as the record of where the copy came from either way. ResubmittedFromID *string `gorm:"type:uuid;index" json:"resubmittedFromId,omitempty"` - // Relations - Webhook Webhook `json:"webhook,omitzero"` - Entrypoint Entrypoint `json:"entrypoint,omitzero"` + // Relations. No model marshals the record it belongs to, so + // Webhook and Entrypoint are left out of the JSON. + Webhook Webhook `json:"-"` + Entrypoint Entrypoint `json:"-"` Deliveries []Delivery `json:"deliveries,omitempty"` } diff --git a/internal/database/model_parent_test.go b/internal/database/model_parent_test.go new file mode 100644 index 0000000..055a1b2 --- /dev/null +++ b/internal/database/model_parent_test.go @@ -0,0 +1,65 @@ +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) + assert.NotContains(t, encoded, `"webhook":`) + + 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, `"webhook":`) +} diff --git a/internal/database/model_target.go b/internal/database/model_target.go index 9c5f95f..43b5a4f 100644 --- a/internal/database/model_target.go +++ b/internal/database/model_target.go @@ -34,7 +34,8 @@ type Target struct { MaxRetries int `json:"maxRetries,omitempty"` MaxQueueSize int `json:"maxQueueSize,omitempty"` - // Relations - Webhook Webhook `json:"webhook,omitzero"` + // Relations. No model marshals the record it belongs to: + // Webhook.Targets leads back here, and the JSON could loop. + Webhook Webhook `json:"-"` Deliveries []Delivery `json:"deliveries,omitempty"` } diff --git a/internal/database/model_webhook.go b/internal/database/model_webhook.go index eedb192..896579d 100644 --- a/internal/database/model_webhook.go +++ b/internal/database/model_webhook.go @@ -66,8 +66,9 @@ type Webhook struct { // must equal DefaultRetentionDays. RetentionDays int `gorm:"default:30" json:"retentionDays"` - // Relations - User User `json:"user,omitzero"` + // Relations. No model marshals the record it belongs to: + // User.Webhooks leads back here, and the JSON could loop. + User User `json:"-"` Entrypoints []Entrypoint `json:"entrypoints,omitempty"` Targets []Target `json:"targets,omitempty"` }