From 66e5333fc42da1ada446fc02407d7ef757a63ba2 Mon Sep 17 00:00:00 2001 From: Phoebe Yu Date: Wed, 28 Jan 2026 14:11:40 -0800 Subject: [PATCH] Add helper function to create update_profile actionSchemas compatible with conventions used for attribute push --- pkg/actions/actions.go | 69 ++++++++++++++++++++++++++++ pkg/actions/actions_test.go | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go index 5d9977184..b9c810b2c 100644 --- a/pkg/actions/actions.go +++ b/pkg/actions/actions.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sort" + "strings" "sync" "time" @@ -14,6 +15,8 @@ import ( "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "github.com/segmentio/ksuid" "go.uber.org/zap" + "golang.org/x/text/cases" + "golang.org/x/text/language" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" @@ -633,3 +636,69 @@ func isNullValue(v *structpb.Value) bool { _, isNull := v.GetKind().(*structpb.Value_NullValue) return isNull } + +// toDisplayName converts a snake_case field name to a Title Case display name. +func toDisplayName(name string) string { + // Replace underscores with spaces, then apply title case + spaced := strings.ReplaceAll(name, "_", " ") + return cases.Title(language.English).String(spaced) +} + +// NewUpdateProfileSchema creates a BatonActionSchema for updating user profiles. +// It follows the convention for ACTION_TYPE_ACCOUNT_UPDATE_PROFILE actions: +// - Named "update_profile" +// - Includes a "user_id" ResourceIdField +// - Includes StringField for each known attribute name +// - Optionally includes "custom_attributes" StringMapField if allowsCustom is true +func NewUpdateProfileSchema(allowsCustom bool, attributeNames []string) *v2.BatonActionSchema { + arguments := []*config.Field{ + config.Field_builder{ + Name: "user_id", + DisplayName: "User ID", + Description: "The user to update", + IsRequired: true, + ResourceIdField: &config.ResourceIdField{ + Rules: &config.ResourceIDRules{ + AllowedResourceTypeIds: []string{"user"}, + }, + }, + }.Build(), + } + + // Dedupe attributeNames and remove reserved names + seen := make(map[string]bool) + reservedNames := map[string]bool{"user_id": true, "custom_attributes": true} + filteredNames := make([]string, 0, len(attributeNames)) + for _, name := range attributeNames { + if reservedNames[name] || seen[name] { + continue + } + seen[name] = true + filteredNames = append(filteredNames, name) + } + + for _, name := range filteredNames { + arguments = append(arguments, config.Field_builder{ + Name: name, + DisplayName: toDisplayName(name), + StringField: &config.StringField{}, + }.Build()) + } + + if allowsCustom { + arguments = append(arguments, config.Field_builder{ + Name: "custom_attributes", + DisplayName: "Custom Attributes", + Description: "Additional custom attributes to set on the user", + StringMapField: &config.StringMapField{}, + }.Build()) + } + + return v2.BatonActionSchema_builder{ + Name: "update_profile", + DisplayName: "Update Profile", + Description: "Update a user's profile attributes", + ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_ACCOUNT_UPDATE_PROFILE}, + Arguments: arguments, + }.Build() +} diff --git a/pkg/actions/actions_test.go b/pkg/actions/actions_test.go index 88036767a..e41574e5a 100644 --- a/pkg/actions/actions_test.go +++ b/pkg/actions/actions_test.go @@ -546,3 +546,95 @@ func TestActionHandlerGoroutineLeaks(t *testing.T) { require.LessOrEqual(t, finalCount, initialCount+1, "goroutine leak detected after context cancellation") }) } + +func TestNewUpdateProfileSchema(t *testing.T) { + t.Run("basic schema without custom attributes", func(t *testing.T) { + schema := NewUpdateProfileSchema(false, nil) + + require.Equal(t, "update_profile", schema.GetName()) + require.Equal(t, "Update Profile", schema.GetDisplayName()) + require.Equal(t, "Update a user's profile attributes", schema.GetDescription()) + require.Contains(t, schema.GetActionType(), v2.ActionType_ACTION_TYPE_ACCOUNT_UPDATE_PROFILE) + + // Should have only user_id field + require.Len(t, schema.GetArguments(), 1) + + userIdField := schema.GetArguments()[0] + require.Equal(t, "user_id", userIdField.GetName()) + require.Equal(t, "User ID", userIdField.GetDisplayName()) + require.True(t, userIdField.GetIsRequired()) + require.NotNil(t, userIdField.GetResourceIdField()) + require.Equal(t, []string{"user"}, userIdField.GetResourceIdField().GetRules().GetAllowedResourceTypeIds()) + }) + + t.Run("schema with attribute names", func(t *testing.T) { + schema := NewUpdateProfileSchema(false, []string{"first_name", "last_name", "email"}) + + // Should have user_id + 3 attribute fields + require.Len(t, schema.GetArguments(), 4) + + // Verify user_id is first + require.Equal(t, "user_id", schema.GetArguments()[0].GetName()) + + // Verify attribute fields + firstNameField := schema.GetArguments()[1] + require.Equal(t, "first_name", firstNameField.GetName()) + require.Equal(t, "First Name", firstNameField.GetDisplayName()) + require.NotNil(t, firstNameField.GetStringField()) + + lastNameField := schema.GetArguments()[2] + require.Equal(t, "last_name", lastNameField.GetName()) + require.Equal(t, "Last Name", lastNameField.GetDisplayName()) + require.NotNil(t, lastNameField.GetStringField()) + + emailField := schema.GetArguments()[3] + require.Equal(t, "email", emailField.GetName()) + require.Equal(t, "Email", emailField.GetDisplayName()) + require.NotNil(t, emailField.GetStringField()) + }) + + t.Run("schema with custom attributes enabled", func(t *testing.T) { + schema := NewUpdateProfileSchema(true, []string{"first_name"}) + + // Should have user_id + first_name + custom_attributes + require.Len(t, schema.GetArguments(), 3) + + customAttrsField := schema.GetArguments()[2] + require.Equal(t, "custom_attributes", customAttrsField.GetName()) + require.Equal(t, "Custom Attributes", customAttrsField.GetDisplayName()) + require.NotNil(t, customAttrsField.GetStringMapField()) + }) + + t.Run("schema without custom attributes disabled", func(t *testing.T) { + schema := NewUpdateProfileSchema(false, []string{"first_name"}) + + // Should have user_id + first_name only + require.Len(t, schema.GetArguments(), 2) + + // Verify no custom_attributes field + for _, field := range schema.GetArguments() { + require.NotEqual(t, "custom_attributes", field.GetName()) + } + }) +} + +func TestToDisplayName(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"first_name", "First Name"}, + {"last_name", "Last Name"}, + {"email", "Email"}, + {"user_profile_picture_url", "User Profile Picture Url"}, + {"id", "Id"}, + {"", ""}, + } + + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + result := toDisplayName(tc.input) + require.Equal(t, tc.expected, result) + }) + } +}