Skip to content
Draft
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
30 changes: 30 additions & 0 deletions .claude/skills/mendix/write-lint-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ silently return empty results (issue #721).
| `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) |
| `rest_clients()` | list of rest_client | Consumed REST service documents (excluding platform modules) |
| `rest_operations()` | list of rest_operation | Operations on consumed REST services, including their `timeout` |
| `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 @@ -321,6 +323,34 @@ def count_not(node):
| `module_name` | string | `"Sales"` |
| `entity_ref` | string | Referenced entity qualified name |

### rest_client
| Property | Type | Example |
|----------|------|---------|
| `id` | string | Document UUID |
| `name` | string | `"CustomerApi"` |
| `qualified_name` | string | `"Sales.CustomerApi"` |
| `module_name` | string | `"Sales"` |
| `folder` | string | Folder path within module |
| `base_url` | string | `"https://api.example.com/v1"` |
| `auth_scheme` | string | Authentication scheme, empty when none |
| `operation_count` | int | Number of operations on the service |
| `documentation` | string | Documentation text |

### rest_operation
| Property | Type | Example |
|----------|------|---------|
| `id` | string | Operation UUID |
| `service_id` | string | Owning service UUID |
| `service_qualified_name` | string | `"Sales.CustomerApi"` |
| `name` | string | `"GetCustomer"` |
| `http_method` | string | `"GET"`, `"POST"`, … |
| `path` | string | `"/customers/{id}"` |
| `parameter_count` | int | Number of parameters |
| `has_body` | bool | True when the request carries a body |
| `response_type` | string | Response type name |
| `timeout` | int | Configured timeout in milliseconds; `0` when none is set |
| `module_name` | string | `"Sales"` |

### permission

Returned by `permissions()` (all types) or `permissions_for()` (entity-specific).
Expand Down
121 changes: 121 additions & 0 deletions mdl/linter/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,127 @@ func (ctx *LintContext) DatabaseConnections() iter.Seq[DatabaseConnection] {
}
}

// RestClient represents a consumed REST service document from the
// rest_clients table.
type RestClient struct {
ID string
Name string
QualifiedName string
ModuleName string
Folder string
BaseUrl string
AuthScheme string
OperationCount int
Documentation string
}

// RestClients returns an iterator over all consumed REST services
// (excluding platform modules).
//
// Backed by the catalog rather than the reader, like DatabaseConnections.
func (ctx *LintContext) RestClients() iter.Seq[RestClient] {
return func(yield func(RestClient) bool) {
rows, err := ctx.db.Query(fmt.Sprintf(`
SELECT rc.Id, rc.Name, rc.QualifiedName, rc.ModuleName, rc.Folder,
rc.BaseUrl, rc.AuthScheme, rc.OperationCount, rc.Documentation
FROM rest_clients rc
LEFT JOIN modules m ON rc.ModuleName = m.Name
WHERE %s
ORDER BY rc.ModuleName, rc.Name
`, notPlatformModule("m")))
if err != nil {
ctx.recordQueryError("RestClients", err)
return
}
defer rows.Close()

for rows.Next() {
var rc RestClient
var folder, baseURL, authScheme, documentation sql.NullString
err := rows.Scan(&rc.ID, &rc.Name, &rc.QualifiedName, &rc.ModuleName, &folder,
&baseURL, &authScheme, &rc.OperationCount, &documentation)
if err != nil {
ctx.recordQueryError("RestClients (row scan)", err)
continue
}
rc.Folder = folder.String
rc.BaseUrl = baseURL.String
rc.AuthScheme = authScheme.String
rc.Documentation = documentation.String

if ctx.IsExcluded(rc.ModuleName) {
continue
}

if !yield(rc) {
return
}
}
}
}

// RestOperation represents one operation on a consumed REST service.
type RestOperation struct {
ID string
ServiceID string
ServiceQualifiedName string
Name string
HttpMethod string
Path string
ParameterCount int
HasBody bool
ResponseType string
// Timeout is the configured timeout in milliseconds; 0 when none is set.
Timeout int
ModuleName string
}

// RestOperations returns an iterator over all consumed REST operations
// (excluding platform modules).
func (ctx *LintContext) RestOperations() iter.Seq[RestOperation] {
return func(yield func(RestOperation) bool) {
rows, err := ctx.db.Query(fmt.Sprintf(`
SELECT ro.Id, ro.ServiceId, ro.ServiceQualifiedName, ro.Name,
ro.HttpMethod, ro.Path, ro.ParameterCount, ro.HasBody,
ro.ResponseType, ro.Timeout, ro.ModuleName
FROM rest_operations ro
LEFT JOIN modules m ON ro.ModuleName = m.Name
WHERE %s
ORDER BY ro.ServiceQualifiedName, ro.Name
`, notPlatformModule("m")))
if err != nil {
ctx.recordQueryError("RestOperations", err)
return
}
defer rows.Close()

for rows.Next() {
var ro RestOperation
var hasBody int
var httpMethod, path, responseType sql.NullString
err := rows.Scan(&ro.ID, &ro.ServiceID, &ro.ServiceQualifiedName, &ro.Name,
&httpMethod, &path, &ro.ParameterCount, &hasBody,
&responseType, &ro.Timeout, &ro.ModuleName)
if err != nil {
ctx.recordQueryError("RestOperations (row scan)", err)
continue
}
ro.HttpMethod = httpMethod.String
ro.Path = path.String
ro.ResponseType = responseType.String
ro.HasBody = hasBody != 0

if ctx.IsExcluded(ro.ModuleName) {
continue
}

if !yield(ro) {
return
}
}
}
}

// Activity represents an activity from the activities table (FULL catalog mode).
type Activity struct {
ID string
Expand Down
62 changes: 62 additions & 0 deletions mdl/linter/starlark.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ func (r *StarlarkRule) buildPredeclared() starlark.StringDict {
"permissions_for": starlark.NewBuiltin("permissions_for", r.builtinPermissionsFor),
"snippets": starlark.NewBuiltin("snippets", r.builtinSnippets),
"database_connections": starlark.NewBuiltin("database_connections", r.builtinDatabaseConnections),
"rest_clients": starlark.NewBuiltin("rest_clients", r.builtinRestClients),
"rest_operations": starlark.NewBuiltin("rest_operations", r.builtinRestOperations),
"activities_for": starlark.NewBuiltin("activities_for", r.builtinActivitiesFor),

// Project-level queries
Expand Down Expand Up @@ -655,6 +657,34 @@ func (r *StarlarkRule) builtinDatabaseConnections(_ *starlark.Thread, _ *starlar
return starlark.NewList(connections), nil
}

// builtinRestClients returns all consumed REST service documents.
func (r *StarlarkRule) builtinRestClients(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if r.ctx == nil {
return starlark.NewList(nil), nil
}

var clients []starlark.Value
for rc := range r.ctx.RestClients() {
clients = append(clients, restClientToStarlark(rc))
}

return starlark.NewList(clients), nil
}

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

var operations []starlark.Value
for ro := range r.ctx.RestOperations() {
operations = append(operations, restOperationToStarlark(ro))
}

return starlark.NewList(operations), 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 {
Expand Down Expand Up @@ -1056,6 +1086,38 @@ func activityToStarlark(a Activity) starlark.Value {
})
}

// restClientToStarlark converts a RestClient to a Starlark struct.
func restClientToStarlark(rc RestClient) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String("rest_client"), starlark.StringDict{
"id": starlark.String(rc.ID),
"name": starlark.String(rc.Name),
"qualified_name": starlark.String(rc.QualifiedName),
"module_name": starlark.String(rc.ModuleName),
"folder": starlark.String(rc.Folder),
"base_url": starlark.String(rc.BaseUrl),
"auth_scheme": starlark.String(rc.AuthScheme),
"operation_count": starlark.MakeInt(rc.OperationCount),
"documentation": starlark.String(rc.Documentation),
})
}

// restOperationToStarlark converts a RestOperation to a Starlark struct.
func restOperationToStarlark(ro RestOperation) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String("rest_operation"), starlark.StringDict{
"id": starlark.String(ro.ID),
"service_id": starlark.String(ro.ServiceID),
"service_qualified_name": starlark.String(ro.ServiceQualifiedName),
"name": starlark.String(ro.Name),
"http_method": starlark.String(ro.HttpMethod),
"path": starlark.String(ro.Path),
"parameter_count": starlark.MakeInt(ro.ParameterCount),
"has_body": starlark.Bool(ro.HasBody),
"response_type": starlark.String(ro.ResponseType),
"timeout": starlark.MakeInt(ro.Timeout),
"module_name": starlark.String(ro.ModuleName),
})
}

// databaseConnectionToStarlark converts a DatabaseConnection to a Starlark struct.
func databaseConnectionToStarlark(dc DatabaseConnection) starlark.Value {
return starlarkstruct.FromStringDict(starlark.String("database_connection"), starlark.StringDict{
Expand Down