Add updated-from entity snapshot to updated webhooks
Update webhook events now carry a top-level "updatedFrom" field alongside "data", containing a full snapshot of the entity as it was before the update. This lets subscribers diff old vs new state (for example the prior membership role on user:updated) without tracking prior state themselves. It is a complete snapshot with the same shape as "data", not a partial diff, so consumers select whatever fields they need. The field is omitted for non-update events. The webhook_data table gains a nullable updated_from JSONB column, and webhook.InsertUpdateData enqueues both snapshots; InsertData delegates to it with a nil updatedFrom so non-update callers are unaffected. Each *:updated emission site snapshots the entity right after load, before mutation: obligation, third-party, user (org and SCIM flows), document, document-version, and document-version-approval-quorum. The document emit helpers gained an optional updatedFrom argument threaded through to the payload. For document-version-approval-quorum:updated the snapshot requires an extra query, so it is now gated behind the same subscription-existence check the emitter uses: when no subscriber is configured the load is skipped entirely rather than running (and potentially failing the approval) for an event nobody receives. Add integration tests (against a real Postgres, skipped when none is reachable) covering the updated_from round-trip, the SQL NULL behavior when no snapshot is provided, and the no-op when no subscription matches, plus a unit test asserting updatedFrom is omitted from the payload when absent. Document the new field in the probod and n8n changelogs and the n8n README. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -38,6 +38,7 @@ type Payload struct {
|
||||
EventType string `json:"eventType"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
UpdatedFrom json.RawMessage `json:"updatedFrom,omitempty"`
|
||||
}
|
||||
|
||||
func InsertData(
|
||||
@@ -47,6 +48,22 @@ func InsertData(
|
||||
organizationID gid.GID,
|
||||
eventType coredata.WebhookEventType,
|
||||
data any,
|
||||
) error {
|
||||
if err := InsertUpdateData(ctx, tx, scope, organizationID, eventType, data, nil); err != nil {
|
||||
return fmt.Errorf("cannot insert webhook data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertUpdateData(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
scope coredata.Scoper,
|
||||
organizationID gid.GID,
|
||||
eventType coredata.WebhookEventType,
|
||||
data any,
|
||||
updatedFrom any,
|
||||
) error {
|
||||
var configs coredata.WebhookSubscriptions
|
||||
|
||||
@@ -64,11 +81,20 @@ func InsertData(
|
||||
return fmt.Errorf("cannot marshal webhook event data: %w", err)
|
||||
}
|
||||
|
||||
var updatedFromRaw json.RawMessage
|
||||
if updatedFrom != nil {
|
||||
updatedFromRaw, err = json.Marshal(updatedFrom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal webhook event updated-from data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
webhookData := &coredata.WebhookData{
|
||||
ID: gid.New(scope.GetTenantID(), coredata.WebhookDataEntityType),
|
||||
OrganizationID: organizationID,
|
||||
EventType: eventType,
|
||||
Data: raw,
|
||||
UpdatedFrom: updatedFromRaw,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user