Skip to content

Commit f958d21

Browse files
authored
feat: destination metadata & delivery_metadata (#542)
* feat: add delivery_metadata and metadata fields to Destination model Add two new fields to the Destination model: - delivery_metadata: Static key-value pairs merged into event data on delivery - metadata: Arbitrary contextual information stored but not sent with events Both fields are map[string]string for simplicity and consistency with existing Config and Credentials fields. The metadata field reuses the existing Metadata type from event.go. * feat: add delivery_metadata and metadata fields to Destination model - Add DeliveryMetadata and Metadata fields to Destination struct - Implement Redis persistence for both fields in UpsertDestination - Add deserialization logic in parseRedisHash to handle optional fields - Update test suite to verify persistence of new fields - Add test coverage for nil field handling * feat: encrypt delivery_metadata field * feat: add delivery_metadata and metadata fields to destination API * feat: implement delivery_metadata merging in BasePublisher * feat: add delivery_metadata support to all destination providers * test: verify delivery_metadata * chore: update openapi.yaml * docs: document metadata & delivery_metadata
1 parent 3ff19ab commit f958d21

File tree

25 files changed

+1149
-48
lines changed

25 files changed

+1149
-48
lines changed

cmd/e2e/api_test.go

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ func (suite *basicSuite) TestTenantsAPI() {
274274
func (suite *basicSuite) TestDestinationsAPI() {
275275
tenantID := idgen.String()
276276
sampleDestinationID := idgen.Destination()
277+
destinationWithMetadataID := idgen.Destination()
277278
tests := []APITest{
278279
{
279280
Name: "PUT /:tenantID",
@@ -438,6 +439,177 @@ func (suite *basicSuite) TestDestinationsAPI() {
438439
},
439440
},
440441
},
442+
{
443+
Name: "POST /:tenantID/destinations with delivery_metadata and metadata",
444+
Request: suite.AuthRequest(httpclient.Request{
445+
Method: httpclient.MethodPOST,
446+
Path: "/" + tenantID + "/destinations",
447+
Body: map[string]interface{}{
448+
"id": destinationWithMetadataID,
449+
"type": "webhook",
450+
"topics": []string{"user.created"},
451+
"config": map[string]interface{}{
452+
"url": "http://host.docker.internal:4444",
453+
},
454+
"delivery_metadata": map[string]interface{}{
455+
"X-App-ID": "test-app",
456+
"X-Version": "1.0",
457+
},
458+
"metadata": map[string]interface{}{
459+
"environment": "test",
460+
"team": "platform",
461+
},
462+
},
463+
}),
464+
Expected: APITestExpectation{
465+
Match: &httpclient.Response{
466+
StatusCode: http.StatusCreated,
467+
},
468+
},
469+
},
470+
{
471+
Name: "GET /:tenantID/destinations/:destinationID with delivery_metadata and metadata",
472+
Request: suite.AuthRequest(httpclient.Request{
473+
Method: httpclient.MethodGET,
474+
Path: "/" + tenantID + "/destinations/" + destinationWithMetadataID,
475+
}),
476+
Expected: APITestExpectation{
477+
Match: &httpclient.Response{
478+
StatusCode: http.StatusOK,
479+
Body: map[string]interface{}{
480+
"id": destinationWithMetadataID,
481+
"type": "webhook",
482+
"topics": []string{"user.created"},
483+
"config": map[string]interface{}{
484+
"url": "http://host.docker.internal:4444",
485+
},
486+
"credentials": map[string]interface{}{},
487+
"delivery_metadata": map[string]interface{}{
488+
"X-App-ID": "test-app",
489+
"X-Version": "1.0",
490+
},
491+
"metadata": map[string]interface{}{
492+
"environment": "test",
493+
"team": "platform",
494+
},
495+
},
496+
},
497+
},
498+
},
499+
{
500+
Name: "PATCH /:tenantID/destinations/:destinationID update delivery_metadata",
501+
Request: suite.AuthRequest(httpclient.Request{
502+
Method: httpclient.MethodPATCH,
503+
Path: "/" + tenantID + "/destinations/" + destinationWithMetadataID,
504+
Body: map[string]interface{}{
505+
"delivery_metadata": map[string]interface{}{
506+
"X-Version": "2.0", // Overwrite existing value (was "1.0")
507+
"X-Region": "us-east-1", // Add new key
508+
},
509+
// Note: X-App-ID not included, should be preserved from original
510+
},
511+
}),
512+
Expected: APITestExpectation{
513+
Match: &httpclient.Response{
514+
StatusCode: http.StatusOK,
515+
Body: map[string]interface{}{
516+
"id": destinationWithMetadataID,
517+
"type": "webhook",
518+
"topics": []string{"user.created"},
519+
"config": map[string]interface{}{
520+
"url": "http://host.docker.internal:4444",
521+
},
522+
"credentials": map[string]interface{}{},
523+
"delivery_metadata": map[string]interface{}{
524+
"X-App-ID": "test-app", // PRESERVED: Not in PATCH request
525+
"X-Version": "2.0", // OVERWRITTEN: Updated from "1.0"
526+
"X-Region": "us-east-1", // NEW: Added by PATCH request
527+
},
528+
"metadata": map[string]interface{}{
529+
"environment": "test",
530+
"team": "platform",
531+
},
532+
},
533+
},
534+
},
535+
},
536+
{
537+
Name: "PATCH /:tenantID/destinations/:destinationID update metadata",
538+
Request: suite.AuthRequest(httpclient.Request{
539+
Method: httpclient.MethodPATCH,
540+
Path: "/" + tenantID + "/destinations/" + destinationWithMetadataID,
541+
Body: map[string]interface{}{
542+
"metadata": map[string]interface{}{
543+
"team": "engineering", // Overwrite existing value (was "platform")
544+
"region": "us", // Add new key
545+
},
546+
// Note: environment not included, should be preserved from original
547+
},
548+
}),
549+
Expected: APITestExpectation{
550+
Match: &httpclient.Response{
551+
StatusCode: http.StatusOK,
552+
Body: map[string]interface{}{
553+
"id": destinationWithMetadataID,
554+
"type": "webhook",
555+
"topics": []string{"user.created"},
556+
"config": map[string]interface{}{
557+
"url": "http://host.docker.internal:4444",
558+
},
559+
"credentials": map[string]interface{}{},
560+
"delivery_metadata": map[string]interface{}{
561+
"X-App-ID": "test-app",
562+
"X-Version": "2.0",
563+
"X-Region": "us-east-1",
564+
},
565+
"metadata": map[string]interface{}{
566+
"environment": "test", // PRESERVED: Not in PATCH request
567+
"team": "engineering", // OVERWRITTEN: Updated from "platform"
568+
"region": "us", // NEW: Added by PATCH request
569+
},
570+
},
571+
},
572+
},
573+
},
574+
{
575+
Name: "GET /:tenantID/destinations/:destinationID verify merged fields",
576+
Request: suite.AuthRequest(httpclient.Request{
577+
Method: httpclient.MethodGET,
578+
Path: "/" + tenantID + "/destinations/" + destinationWithMetadataID,
579+
}),
580+
Expected: APITestExpectation{
581+
Match: &httpclient.Response{
582+
StatusCode: http.StatusOK,
583+
Body: map[string]interface{}{
584+
"id": destinationWithMetadataID,
585+
"type": "webhook",
586+
"topics": []string{"user.created"},
587+
"config": map[string]interface{}{
588+
"url": "http://host.docker.internal:4444",
589+
},
590+
"credentials": map[string]interface{}{},
591+
// Verify delivery_metadata merge behavior persists:
592+
// - Original: {"X-App-ID": "test-app", "X-Version": "1.0"}
593+
// - After PATCH 1: {"X-Version": "2.0", "X-Region": "us-east-1"}
594+
// - Result: Preserved X-App-ID, overwrote X-Version, added X-Region
595+
"delivery_metadata": map[string]interface{}{
596+
"X-App-ID": "test-app",
597+
"X-Version": "2.0",
598+
"X-Region": "us-east-1",
599+
},
600+
// Verify metadata merge behavior persists:
601+
// - Original: {"environment": "test", "team": "platform"}
602+
// - After PATCH 2: {"team": "engineering", "region": "us"}
603+
// - Result: Preserved environment, overwrote team, added region
604+
"metadata": map[string]interface{}{
605+
"environment": "test",
606+
"team": "engineering",
607+
"region": "us",
608+
},
609+
},
610+
},
611+
},
612+
},
441613
{
442614
Name: "POST /:tenantID/destinations with duplicate ID",
443615
Request: suite.AuthRequest(httpclient.Request{
@@ -468,7 +640,7 @@ func (suite *basicSuite) TestDestinationsAPI() {
468640
Path: "/" + tenantID + "/destinations",
469641
}),
470642
Expected: APITestExpectation{
471-
Validate: makeDestinationListValidator(2),
643+
Validate: makeDestinationListValidator(3),
472644
},
473645
},
474646
{
@@ -621,7 +793,7 @@ func (suite *basicSuite) TestDestinationsAPI() {
621793
Path: "/" + tenantID + "/destinations",
622794
}),
623795
Expected: APITestExpectation{
624-
Validate: makeDestinationListValidator(1),
796+
Validate: makeDestinationListValidator(2),
625797
},
626798
},
627799
}

0 commit comments

Comments
 (0)