Skip to content

Commit 97d3af9

Browse files
committed
Dynamic schema support and DB fallback to Email Executor
Signed-off-by: RandithaK <me@randitha.net>
1 parent 4f22331 commit 97d3af9

6 files changed

Lines changed: 504 additions & 68 deletions

File tree

backend/internal/flow/common/constants.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ const (
193193
RuntimeKeyUserAttributesCacheTTLSeconds = "user_attributes_cache_ttl_seconds"
194194
// RuntimeKeyInviteLink holds the generated invite link for downstream executors (e.g., EmailExecutor).
195195
RuntimeKeyInviteLink = "inviteLink"
196+
// RuntimeKeySkipDelivery indicates that delivery should be skipped for the current flow.
197+
RuntimeKeySkipDelivery = "skipDelivery"
196198
// RuntimeKeyCandidateUsers holds serialized candidate users during disambiguation in resolve mode.
197199
RuntimeKeyCandidateUsers = "candidateUsers"
198200
)
@@ -203,6 +205,8 @@ const (
203205
const (
204206
// InputTypeText represents a text input type.
205207
InputTypeText = "TEXT_INPUT"
208+
// InputTypeEmail represents an email input type.
209+
InputTypeEmail = "EMAIL_INPUT"
206210
// InputTypePassword represents a password credential input type.
207211
InputTypePassword = "PASSWORD_INPUT"
208212
// InputTypeOTP represents a one-time password input type.
@@ -224,6 +228,10 @@ const (
224228
// AttributeMobileNumber is the default attribute name for a user's mobile phone number.
225229
AttributeMobileNumber = "mobileNumber"
226230
)
231+
const (
232+
// AttributeEmail is the default attribute name for a user's email.
233+
AttributeEmail = "email"
234+
)
227235

228236
// sensitiveInputTypes contains the list of input types that are considered sensitive.
229237
var sensitiveInputTypes = []string{

backend/internal/flow/executor/email_executor.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"errors"
2323
"fmt"
2424

25+
"github.com/asgardeo/thunder/internal/entityprovider"
2526
"github.com/asgardeo/thunder/internal/flow/common"
2627
"github.com/asgardeo/thunder/internal/flow/core"
2728
"github.com/asgardeo/thunder/internal/system/email"
@@ -36,11 +37,21 @@ type emailExecutor struct {
3637
logger *log.Logger
3738
emailClient email.EmailClientInterface
3839
templateService template.TemplateServiceInterface
40+
entityProvider entityprovider.EntityProviderInterface
41+
}
42+
43+
// defaultEmailInput is the default input definition for email collection.
44+
var defaultEmailInput = common.Input{
45+
Ref: "email_input",
46+
Identifier: common.AttributeEmail,
47+
Type: common.InputTypeEmail,
48+
Required: true,
3949
}
4050

4151
// newEmailExecutor creates a new instance of the email executor.
4252
func newEmailExecutor(flowFactory core.FlowFactoryInterface, emailClient email.EmailClientInterface,
43-
templateService template.TemplateServiceInterface) *emailExecutor {
53+
templateService template.TemplateServiceInterface,
54+
entityProvider entityprovider.EntityProviderInterface) *emailExecutor {
4455
logger := log.GetLogger().With(log.String(log.LoggerKeyComponentName, "EmailExecutor"))
4556
base := flowFactory.CreateExecutor(
4657
ExecutorNameEmailExecutor,
@@ -55,6 +66,7 @@ func newEmailExecutor(flowFactory core.FlowFactoryInterface, emailClient email.E
5566
logger: logger,
5667
emailClient: emailClient,
5768
templateService: templateService,
69+
entityProvider: entityProvider,
5870
}
5971
}
6072

@@ -79,6 +91,12 @@ func (e *emailExecutor) executeSend(ctx *core.NodeContext) (*common.ExecutorResp
7991
RuntimeData: make(map[string]string),
8092
}
8193

94+
if ctx.RuntimeData[common.RuntimeKeySkipDelivery] == dataValueTrue {
95+
logger.Debug("Delivery marked as skipped, completing without sending email")
96+
execResp.Status = common.ExecComplete
97+
return execResp, nil
98+
}
99+
82100
if e.emailClient == nil {
83101
execResp.AdditionalData[common.DataEmailSent] = dataValueFalse
84102
execResp.Status = common.ExecFailure
@@ -92,7 +110,7 @@ func (e *emailExecutor) executeSend(ctx *core.NodeContext) (*common.ExecutorResp
92110
}
93111

94112
// Resolve recipient email from user inputs or runtime data.
95-
recipient := resolveRecipientEmail(ctx)
113+
recipient := e.resolveRecipientEmail(ctx)
96114
if recipient == "" {
97115
logger.Debug("Email recipient not found in user inputs or runtime data")
98116
execResp.Status = common.ExecFailure
@@ -125,7 +143,6 @@ func (e *emailExecutor) executeSend(ctx *core.NodeContext) (*common.ExecutorResp
125143
"inviteLink": inviteLink,
126144
"appName": ctx.Application.Name,
127145
}
128-
129146
rendered, svcErr := e.templateService.Render(ctx.Context, scenario, template.TemplateTypeEmail, templateData)
130147
if svcErr != nil {
131148
return nil, fmt.Errorf("failed to render email template: %s", svcErr.Code)
@@ -156,14 +173,36 @@ func (e *emailExecutor) executeSend(ctx *core.NodeContext) (*common.ExecutorResp
156173
return execResp, nil
157174
}
158175

159-
// resolveRecipientEmail retrieves the recipient email from user inputs or runtime data.
160-
func resolveRecipientEmail(ctx *core.NodeContext) string {
161-
if recipientEmail, ok := ctx.UserInputs[userAttributeEmail]; ok && recipientEmail != "" {
176+
// resolveRecipientEmail retrieves the recipient email from user inputs, runtime data, or forwarded data.
177+
func (e *emailExecutor) resolveRecipientEmail(ctx *core.NodeContext) string {
178+
emailAttr := e.resolveEmailInput(ctx).Identifier
179+
180+
if recipientEmail, ok := ctx.UserInputs[emailAttr]; ok && recipientEmail != "" {
162181
return recipientEmail
163182
}
164-
if recipientEmail, ok := ctx.RuntimeData[userAttributeEmail]; ok && recipientEmail != "" {
183+
if recipientEmail, ok := ctx.RuntimeData[emailAttr]; ok && recipientEmail != "" {
165184
return recipientEmail
166185
}
186+
if recipientEmail, ok := ctx.ForwardedData[emailAttr]; ok {
187+
if emailStr, isString := recipientEmail.(string); isString && emailStr != "" {
188+
return emailStr
189+
}
190+
}
191+
192+
if userID, ok := ctx.RuntimeData[userAttributeUserID]; ok && userID != "" && e.entityProvider != nil {
193+
user, providerErr := e.entityProvider.GetEntity(userID)
194+
if providerErr != nil {
195+
e.logger.Error("Failed to fetch user from entity provider for email resolution", log.Error(providerErr))
196+
} else if user != nil {
197+
if recipientEmail, err := GetUserAttribute(user, emailAttr); err == nil {
198+
return recipientEmail
199+
} else {
200+
e.logger.Debug("Email attribute not found in user entity", log.String("attribute", emailAttr),
201+
log.Error(err))
202+
}
203+
}
204+
}
205+
167206
return ""
168207
}
169208

@@ -180,3 +219,14 @@ func isEmailError(err error) bool {
180219
errors.Is(err, email.ErrorSMTPAuth) ||
181220
errors.Is(err, email.ErrorEmailSendFailed)
182221
}
222+
223+
// resolveEmailInput returns the EMAIL_INPUT definition from the node context inputs,
224+
// falling back to the default if none is found.
225+
func (e *emailExecutor) resolveEmailInput(ctx *core.NodeContext) common.Input {
226+
for _, input := range ctx.NodeInputs {
227+
if input.Type == common.InputTypeEmail {
228+
return input
229+
}
230+
}
231+
return defaultEmailInput
232+
}

0 commit comments

Comments
 (0)