Skip to content

Commit 24ea330

Browse files
committed
feat: add call javascript action MDL pipeline and association retrieve fix
Full pipeline for `call javascript action` syntax: - Grammar rule, AST node, visitor, builder, validator, BSON serializer - JavaScript actions allowed in nanoflows (client-side), disallowed check skips them correctly - Separate JS action reference validation from Java action validation Association retrieve roundtrip fix: - Preserve AssociationRetrieveSource on roundtrip instead of converting to DatabaseRetrieveSource with XPath for reverse traversals
1 parent 66f44b2 commit 24ea330

17 files changed

Lines changed: 10652 additions & 10135 deletions

mdl/ast/ast_microflow.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,17 @@ type CallJavaActionStmt struct {
367367

368368
func (s *CallJavaActionStmt) isMicroflowStatement() {}
369369

370+
// CallJavaScriptActionStmt represents: CALL JAVASCRIPT ACTION Name (args) [ON ERROR ...]
371+
type CallJavaScriptActionStmt struct {
372+
OutputVariable string // Optional output variable
373+
ActionName QualifiedName // JavaScript action name
374+
Arguments []CallArgument // Arguments
375+
ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause
376+
Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation
377+
}
378+
379+
func (s *CallJavaScriptActionStmt) isMicroflowStatement() {}
380+
370381
// ExecuteDatabaseQueryStmt represents: EXECUTE DATABASE QUERY Module.Connection.QueryName ...
371382
type ExecuteDatabaseQueryStmt struct {
372383
OutputVariable string // Optional output variable

mdl/executor/cmd_microflows_builder_actions.go

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -286,51 +286,29 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID {
286286

287287
if s.StartVariable != "" {
288288
// Association retrieve: RETRIEVE $List FROM $Parent/Module.AssocName
289+
// Always use AssociationRetrieveSource to preserve the original syntax.
290+
// The runtime resolves traversal direction from association metadata.
289291
assocQN := s.Source.Module + "." + s.Source.Name
290292

291-
// Look up association to determine type and direction.
292-
// For Reference associations, AssociationRetrieveSource always returns a single
293-
// object (the entity on the other end). When the user navigates from the child
294-
// (non-owner) side, the intent is to get a list of parent entities — we must use
295-
// a DatabaseRetrieveSource with XPath constraint instead.
296-
assocInfo := fb.lookupAssociation(s.Source.Module, s.Source.Name)
297-
startVarType := ""
298-
if fb.varTypes != nil {
299-
startVarType = fb.varTypes[s.StartVariable]
293+
source = &microflows.AssociationRetrieveSource{
294+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
295+
StartVariable: s.StartVariable,
296+
AssociationQualifiedName: assocQN,
300297
}
301298

302-
if assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference &&
303-
assocInfo.childEntityQN != "" && startVarType == assocInfo.childEntityQN {
304-
// Reverse traversal on Reference: child → parent (one-to-many)
305-
// Use DatabaseRetrieveSource with XPath to get a list of parent entities
306-
dbSource := &microflows.DatabaseRetrieveSource{
307-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
308-
EntityQualifiedName: assocInfo.parentEntityQN,
309-
XPathConstraint: "[" + assocQN + " = $" + s.StartVariable + "]",
310-
}
311-
source = dbSource
312-
if fb.varTypes != nil {
313-
fb.varTypes[s.Variable] = "List of " + assocInfo.parentEntityQN
314-
}
315-
} else {
316-
// Forward traversal or ReferenceSet: use AssociationRetrieveSource
317-
source = &microflows.AssociationRetrieveSource{
318-
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
319-
StartVariable: s.StartVariable,
320-
AssociationQualifiedName: assocQN,
321-
}
322-
if fb.varTypes != nil {
323-
if assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference {
324-
// Reference forward traversal: returns single object
325-
otherEntity := assocInfo.childEntityQN
326-
if startVarType == assocInfo.childEntityQN {
327-
otherEntity = assocInfo.parentEntityQN
328-
}
329-
fb.varTypes[s.Variable] = otherEntity
330-
} else {
331-
// ReferenceSet or unknown: returns a list
332-
fb.varTypes[s.Variable] = "List of " + assocQN
299+
// Infer variable type for downstream validation
300+
if fb.varTypes != nil {
301+
assocInfo := fb.lookupAssociation(s.Source.Module, s.Source.Name)
302+
startVarType := fb.varTypes[s.StartVariable]
303+
if assocInfo != nil && assocInfo.Type == domainmodel.AssociationTypeReference {
304+
otherEntity := assocInfo.childEntityQN
305+
if startVarType == assocInfo.childEntityQN {
306+
otherEntity = assocInfo.parentEntityQN
333307
}
308+
fb.varTypes[s.Variable] = otherEntity
309+
} else {
310+
// ReferenceSet or unknown: returns a list
311+
fb.varTypes[s.Variable] = "List of " + assocQN
334312
}
335313
}
336314
} else {

mdl/executor/cmd_microflows_builder_annotations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ func getStatementAnnotations(stmt ast.MicroflowStatement) *ast.ActivityAnnotatio
4747
return s.Annotations
4848
case *ast.CallJavaActionStmt:
4949
return s.Annotations
50+
case *ast.CallJavaScriptActionStmt:
51+
return s.Annotations
5052
case *ast.ExecuteDatabaseQueryStmt:
5153
return s.Annotations
5254
case *ast.CallExternalActionStmt:

mdl/executor/cmd_microflows_builder_calls.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,66 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model.
328328
return activity.ID
329329
}
330330

331+
// addCallJavaScriptActionAction creates a CALL JAVASCRIPT ACTION statement.
332+
func (fb *flowBuilder) addCallJavaScriptActionAction(s *ast.CallJavaScriptActionStmt) model.ID {
333+
actionQN := s.ActionName.Module + "." + s.ActionName.Name
334+
335+
// Build parameter mappings with Value structure
336+
var mappings []*microflows.JavaScriptActionParameterMapping
337+
for _, arg := range s.Arguments {
338+
// Parameter qualified name format: Module.JavaScriptAction.ParameterName
339+
paramQN := actionQN + "." + arg.Name
340+
341+
// JavaScript actions use BasicCodeActionParameterValue for all parameters
342+
valueExpr := fb.exprToString(arg.Value)
343+
value := &microflows.BasicCodeActionParameterValue{
344+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
345+
Argument: valueExpr,
346+
}
347+
348+
mapping := &microflows.JavaScriptActionParameterMapping{
349+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
350+
Parameter: paramQN,
351+
Value: value,
352+
}
353+
mappings = append(mappings, mapping)
354+
}
355+
356+
action := &microflows.JavaScriptActionCallAction{
357+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
358+
ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling),
359+
JavaScriptAction: actionQN,
360+
ParameterMappings: mappings,
361+
OutputVariableName: s.OutputVariable,
362+
UseReturnVariable: s.OutputVariable != "",
363+
}
364+
365+
activityX := fb.posX
366+
activity := &microflows.ActionActivity{
367+
BaseActivity: microflows.BaseActivity{
368+
BaseMicroflowObject: microflows.BaseMicroflowObject{
369+
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
370+
Position: model.Point{X: fb.posX, Y: fb.posY},
371+
Size: model.Size{Width: ActivityWidth, Height: ActivityHeight},
372+
},
373+
AutoGenerateCaption: true,
374+
},
375+
Action: action,
376+
}
377+
378+
fb.objects = append(fb.objects, activity)
379+
fb.posX += fb.spacing
380+
381+
// Build custom error handler flow if present
382+
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
383+
errorY := fb.posY + VerticalSpacing
384+
mergeID := fb.addErrorHandlerFlow(activity.ID, activityX, s.ErrorHandling.Body)
385+
fb.handleErrorHandlerMerge(mergeID, activity.ID, errorY)
386+
}
387+
388+
return activity.ID
389+
}
390+
331391
// addCallExternalActionAction creates a CALL EXTERNAL ACTION statement.
332392
func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt) model.ID {
333393
serviceQN := s.ServiceName.Module + "." + s.ServiceName.Name

mdl/executor/cmd_microflows_builder_graph.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ func (fb *flowBuilder) addStatement(stmt ast.MicroflowStatement) model.ID {
224224
return fb.addCallNanoflowAction(s)
225225
case *ast.CallJavaActionStmt:
226226
return fb.addCallJavaActionAction(s)
227+
case *ast.CallJavaScriptActionStmt:
228+
return fb.addCallJavaScriptActionAction(s)
227229
case *ast.ExecuteDatabaseQueryStmt:
228230
return fb.addExecuteDatabaseQueryAction(s)
229231
case *ast.CallExternalActionStmt:

mdl/executor/cmd_microflows_builder_validate.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ func (fb *flowBuilder) validateStatement(stmt ast.MicroflowStatement) {
163163
fb.validateStatements(s.ErrorHandling.Body)
164164
}
165165

166+
case *ast.CallJavaScriptActionStmt:
167+
// Register result variable if assigned
168+
if s.OutputVariable != "" {
169+
fb.declaredVars[s.OutputVariable] = "Unknown"
170+
}
171+
// Validate error handler body if present
172+
if s.ErrorHandling != nil && len(s.ErrorHandling.Body) > 0 {
173+
fb.validateStatements(s.ErrorHandling.Body)
174+
}
175+
166176
case *ast.ExecuteDatabaseQueryStmt:
167177
if s.OutputVariable != "" {
168178
fb.declaredVars[s.OutputVariable] = "Unknown"

mdl/executor/nanoflow_validation.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ func getErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClause {
113113
return s.ErrorHandling
114114
case *ast.CallJavaActionStmt:
115115
return s.ErrorHandling
116+
case *ast.CallJavaScriptActionStmt:
117+
return s.ErrorHandling
116118
case *ast.CallExternalActionStmt:
117119
return s.ErrorHandling
118120
case *ast.RestCallStmt:

mdl/executor/validate.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ func (c *flowRefCollector) collectFromStatements(stmts []ast.MicroflowStatement)
519519
if s.ActionName.Module != "" {
520520
c.javaActions = append(c.javaActions, s.ActionName.String())
521521
}
522+
case *ast.CallJavaScriptActionStmt:
523+
if s.ActionName.Module != "" {
524+
c.javaActions = append(c.javaActions, s.ActionName.String())
525+
}
522526
case *ast.CreateObjectStmt:
523527
if s.EntityType.Module != "" {
524528
c.entities = append(c.entities, entityRef{name: s.EntityType.String(), source: "create"})
@@ -569,6 +573,10 @@ func getErrorHandlerBody(stmt ast.MicroflowStatement) []ast.MicroflowStatement {
569573
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
570574
return s.ErrorHandling.Body
571575
}
576+
case *ast.CallJavaScriptActionStmt:
577+
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
578+
return s.ErrorHandling.Body
579+
}
572580
case *ast.ExecuteDatabaseQueryStmt:
573581
if s.ErrorHandling != nil && s.ErrorHandling.Body != nil {
574582
return s.ErrorHandling.Body

mdl/executor/validate_microflow.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ func stmtActivityName(stmt ast.MicroflowStatement) string {
165165
return "call nanoflow"
166166
case *ast.CallJavaActionStmt:
167167
return "call java action"
168+
case *ast.CallJavaScriptActionStmt:
169+
return "call javascript action"
168170
case *ast.ExecuteDatabaseQueryStmt:
169171
return "execute database query"
170172
default:
@@ -362,6 +364,10 @@ func collectDeclaredVars(body []ast.MicroflowStatement) map[string]bool {
362364
if stmt.OutputVariable != "" {
363365
vars[stmt.OutputVariable] = true
364366
}
367+
case *ast.CallJavaScriptActionStmt:
368+
if stmt.OutputVariable != "" {
369+
vars[stmt.OutputVariable] = true
370+
}
365371
case *ast.ExecuteDatabaseQueryStmt:
366372
if stmt.OutputVariable != "" {
367373
vars[stmt.OutputVariable] = true
@@ -464,6 +470,8 @@ func stmtErrorHandling(stmt ast.MicroflowStatement) *ast.ErrorHandlingClause {
464470
return s.ErrorHandling
465471
case *ast.CallJavaActionStmt:
466472
return s.ErrorHandling
473+
case *ast.CallJavaScriptActionStmt:
474+
return s.ErrorHandling
467475
case *ast.ExecuteDatabaseQueryStmt:
468476
return s.ErrorHandling
469477
}

mdl/grammar/MDLParser.g4

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,7 @@ microflowStatement
13101310
| annotation* callMicroflowStatement SEMICOLON?
13111311
| annotation* callNanoflowStatement SEMICOLON?
13121312
| annotation* callJavaActionStatement SEMICOLON?
1313+
| annotation* callJavaScriptActionStatement SEMICOLON?
13131314
| annotation* executeDatabaseQueryStatement SEMICOLON?
13141315
| annotation* callExternalActionStatement SEMICOLON?
13151316
| annotation* showPageStatement SEMICOLON?
@@ -1482,6 +1483,11 @@ callJavaActionStatement
14821483
: (VARIABLE EQUALS)? CALL JAVA ACTION qualifiedName LPAREN callArgumentList? RPAREN onErrorClause?
14831484
;
14841485

1486+
// $Result = CALL JAVASCRIPT ACTION Module.JSAction(Param = 'value');
1487+
callJavaScriptActionStatement
1488+
: (VARIABLE EQUALS)? CALL JAVASCRIPT ACTION qualifiedName LPAREN callArgumentList? RPAREN onErrorClause?
1489+
;
1490+
14851491
// $Result = EXECUTE DATABASE QUERY Module.Connection.QueryName (param = 'value');
14861492
// $Result = EXECUTE DATABASE QUERY Module.Connection.QueryName DYNAMIC 'SELECT ...';
14871493
// $Result = EXECUTE DATABASE QUERY Module.Connection.QueryName CONNECTION (DBSource = $Url, DBUsername = $User, DBPassword = $Pass);

0 commit comments

Comments
 (0)