Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/skills/mendix/write-lint-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def check():
| `constants()` | list of constant | All non-system constants |
| `widgets()` | list of widget | All non-system widgets |
| `snippets()` | list of snippet | All non-system snippets |
| `scheduled_events()` | list of scheduled_event | All non-system scheduled events (requires MPR reader) |
| `attributes_for(entity_qualified_name)` | list of attribute | Attributes for a specific entity |
| `activities_for(microflow_qualified_name)` | list of activity | Activities for a microflow (requires FULL catalog) |
| `permissions()` | list of permission | All permissions across all element types |
Expand Down Expand Up @@ -182,6 +183,16 @@ def check():
| `folder` | string | `"snippets"` — folder path within module |
| `widget_count` | int | Number of widgets |

### scheduled_event
| Property | Type | Example |
|----------|------|---------|
| `name` | string | `"SE_NightlyCleanup"` |
| `qualified_name` | string | `"MyModule.SE_NightlyCleanup"` |
| `module_name` | string | `"MyModule"` |
| `microflow_name` | string | `"MyModule.MF_NightlyCleanup"` — resolved from catalog; raw UUID when catalog not built |
| `interval_seconds` | int | `86400` — `0` for unrecognised interval type |
| `enabled` | bool | `True` if the event is active |

### attribute
| Property | Type | Example |
|----------|------|---------|
Expand Down
92 changes: 92 additions & 0 deletions mdl/linter/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type LintReader interface {
ListModules() ([]*model.Module, error)
ListFolders() ([]*types.FolderInfo, error)
GetRawUnit(id model.ID) (map[string]any, error)
ListScheduledEvents() ([]*model.ScheduledEvent, error)
}

// LintContext wraps a catalog and provides rule-friendly APIs.
Expand Down Expand Up @@ -731,6 +732,97 @@ func (ctx *LintContext) Snippets() iter.Seq[Snippet] {
}
}

// ScheduledEvent represents a scheduled event document.
type ScheduledEvent struct {
Name string
QualifiedName string
ModuleName string
MicroflowName string // qualified name of the microflow to execute
IntervalSeconds int
Enabled bool
}

// intervalToSeconds converts a Mendix interval value and type to seconds.
// Returns 0 for unrecognised interval types (treated as "not convertible").
func intervalToSeconds(interval int, intervalType string) int {
multipliers := map[string]int{
"Second": 1,
"Minute": 60,
"Hour": 3600,
"Day": 86400,
"Week": 604800,
"Month": 2592000,
"Year": 31536000,
}
if mult, ok := multipliers[intervalType]; ok {
return interval * mult
}
return 0
}

// ScheduledEvents returns an iterator over all scheduled events (excluding system modules).
// Returns an empty iterator if no reader is available.
func (ctx *LintContext) ScheduledEvents() iter.Seq[ScheduledEvent] {
return func(yield func(ScheduledEvent) bool) {
if ctx.reader == nil {
return
}

// Build module ID → name map from catalog.
moduleNames := map[model.ID]string{}
// Build microflow UUID → qualified name map from catalog.
// Falls back to the raw UUID when the catalog has not been built yet.
microflowNames := map[string]string{}
if ctx.db != nil {
if rows, err := ctx.db.Query(`SELECT Id, Name FROM modules`); err == nil {
defer rows.Close()
for rows.Next() {
var id, name string
if rows.Scan(&id, &name) == nil {
moduleNames[model.ID(id)] = name
}
}
}
if rows, err := ctx.db.Query(`SELECT Id, QualifiedName FROM microflows`); err == nil {
defer rows.Close()
for rows.Next() {
var id, qname string
if rows.Scan(&id, &qname) == nil {
microflowNames[id] = qname
}
}
}
}

events, err := ctx.reader.ListScheduledEvents()
if err != nil {
return
}

for _, e := range events {
moduleName := moduleNames[e.ContainerID]
if ctx.IsExcluded(moduleName) {
continue
}
mfName := microflowNames[string(e.MicroflowID)]
if mfName == "" {
mfName = string(e.MicroflowID)
}
se := ScheduledEvent{
Name: e.Name,
QualifiedName: moduleName + "." + e.Name,
ModuleName: moduleName,
MicroflowName: mfName,
IntervalSeconds: intervalToSeconds(e.Interval, e.IntervalType),
Enabled: e.Enabled,
}
if !yield(se) {
return
}
}
}
}

// DatabaseConnection represents a database connection from the catalog.
type DatabaseConnection struct {
ID string
Expand Down
44 changes: 35 additions & 9 deletions mdl/linter/starlark.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,16 @@ func LoadStarlarkRule(path string) (*StarlarkRule, error) {
func (r *StarlarkRule) buildPredeclared() starlark.StringDict {
return starlark.StringDict{
// Query functions
"entities": starlark.NewBuiltin("entities", r.builtinEntities),
"microflows": starlark.NewBuiltin("microflows", r.builtinMicroflows),
"pages": starlark.NewBuiltin("pages", r.builtinPages),
"enumerations": starlark.NewBuiltin("enumerations", r.builtinEnumerations),
"constants": starlark.NewBuiltin("constants", r.builtinConstants),
"widgets": starlark.NewBuiltin("widgets", r.builtinWidgets),
"refs_to": starlark.NewBuiltin("refs_to", r.builtinRefsTo),
"refs_from": starlark.NewBuiltin("refs_from", r.builtinRefsFrom),
"attributes_for": starlark.NewBuiltin("attributes_for", r.builtinAttributesFor),
"entities": starlark.NewBuiltin("entities", r.builtinEntities),
"microflows": starlark.NewBuiltin("microflows", r.builtinMicroflows),
"pages": starlark.NewBuiltin("pages", r.builtinPages),
"enumerations": starlark.NewBuiltin("enumerations", r.builtinEnumerations),
"constants": starlark.NewBuiltin("constants", r.builtinConstants),
"widgets": starlark.NewBuiltin("widgets", r.builtinWidgets),
"refs_to": starlark.NewBuiltin("refs_to", r.builtinRefsTo),
"refs_from": starlark.NewBuiltin("refs_from", r.builtinRefsFrom),
"attributes_for": starlark.NewBuiltin("attributes_for", r.builtinAttributesFor),
"scheduled_events": starlark.NewBuiltin("scheduled_events", r.builtinScheduledEvents),

// Graph-analysis facts (populated by `refresh catalog communities`).
"community_of": starlark.NewBuiltin("community_of", r.builtinCommunityOf),
Expand Down Expand Up @@ -461,6 +462,20 @@ func (r *StarlarkRule) builtinDatabaseConnections(_ *starlark.Thread, _ *starlar
return starlark.NewList(connections), nil
}

// builtinScheduledEvents returns all scheduled events.
func (r *StarlarkRule) builtinScheduledEvents(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if r.ctx == nil {
return starlark.NewList(nil), nil
}

var result []starlark.Value
for se := range r.ctx.ScheduledEvents() {
result = append(result, scheduledEventToStarlark(se))
}

return starlark.NewList(result), nil
}

// builtinActivitiesFor returns the activities for a given microflow.
func (r *StarlarkRule) builtinActivitiesFor(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if r.ctx == nil {
Expand Down Expand Up @@ -789,6 +804,17 @@ func databaseConnectionToStarlark(dc DatabaseConnection) starlark.Value {
})
}

func scheduledEventToStarlark(se ScheduledEvent) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String("scheduled_event"), starlark.StringDict{
"name": starlark.String(se.Name),
"qualified_name": starlark.String(se.QualifiedName),
"module_name": starlark.String(se.ModuleName),
"microflow_name": starlark.String(se.MicroflowName),
"interval_seconds": starlark.MakeInt(se.IntervalSeconds),
"enabled": starlark.Bool(se.Enabled),
})
}

// builtinViolation creates a violation struct.
func builtinViolation(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var message starlark.String
Expand Down
Loading