Skip to content

Commit d44de35

Browse files
authored
Preserve jqQuery wrapper in dataset rule values (#325)
The DatasetValue.MarshalJSON method was incorrectly flattening jqQuery values when sending to the Port API. Instead of outputting the required {"jqQuery": "..."} format, it was outputting raw values. This caused dynamic JQ expressions in dataset rules (like .form.field_name or .user.teams) to be treated as literal strings, breaking entity filtering in self-service actions. Before: {"value": ".form.selected_team"} After: {"value": {"jqQuery": ".form.selected_team"}}
1 parent b944508 commit d44de35

File tree

3 files changed

+255
-16
lines changed

3 files changed

+255
-16
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Dynamic Form Filters Example
2+
3+
This example demonstrates how to use dynamic JQ expressions in dataset rules to create dependent form fields where the options in one field are filtered based on the selection in another field.
4+
5+
## Use Case
6+
7+
When building self-service actions, you often need to filter entity selectors based on:
8+
- User's form selections (`.form.field_name`)
9+
- Current user's context (`.user.teams`, `.user.email`)
10+
- Entity context (`.entity.properties.field`)
11+
12+
## How It Works
13+
14+
1. User selects a team from the "Select Team" dropdown (e.g., "engineering")
15+
2. The "Target Service" entity selector automatically filters to show only services where:
16+
- `team` matches the selected team (dynamic)
17+
- `environment` is NOT "production" (literal string comparison)
18+
3. The filtering is dynamic - changing the team selection updates the available services
19+
20+
## Key Configuration
21+
22+
```hcl
23+
dataset = {
24+
combinator = "and"
25+
rules = [
26+
# Dynamic JQ expression - evaluates form input at runtime
27+
{
28+
property = "team"
29+
operator = "="
30+
value = {
31+
jq_query = ".form.selected_team"
32+
}
33+
},
34+
# Literal string comparison - note the escaped quotes
35+
{
36+
property = "environment"
37+
operator = "!="
38+
value = {
39+
jq_query = "\"production\""
40+
}
41+
}
42+
]
43+
}
44+
```
45+
46+
## JQ Expression Types
47+
48+
| Type | Example | Description |
49+
|------|---------|-------------|
50+
| Dynamic | `.form.selected_team` | Evaluates to the form field value at runtime |
51+
| Literal | `"\"production\""` | A literal string "production" (quotes inside) |
52+
53+
## Running the Example
54+
55+
```bash
56+
# Set credentials
57+
export TF_VAR_port_client_id="your-client-id"
58+
export TF_VAR_port_client_secret="your-client-secret"
59+
60+
# Apply
61+
terraform init
62+
terraform apply
63+
64+
# Cleanup
65+
terraform destroy
66+
```
67+
68+
## Testing
69+
70+
1. Go to Port UI → Self-Service Actions
71+
2. Find "Select Service by Team"
72+
3. Select "engineering" → see only `dev-api-service` (prod is filtered out)
73+
4. Select "platform" → see `staging-web-frontend` and `dev-web-frontend`
74+
75+
## Common JQ Expressions
76+
77+
| Expression | Description |
78+
|------------|-------------|
79+
| `.form.field_name` | Value from another form field |
80+
| `.user.teams` | Current user's team memberships |
81+
| `.user.email` | Current user's email |
82+
| `.entity.identifier` | Current entity's identifier |
83+
| `"literal"` | A literal string value |
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Example: Dynamic Form Filters with JQ Expressions
2+
#
3+
# This example demonstrates how to use dynamic JQ expressions in dataset rules
4+
# to filter entity selectors based on form input values.
5+
6+
terraform {
7+
required_providers {
8+
port = {
9+
source = "port-labs/port-labs"
10+
version = "~> 2.0"
11+
}
12+
}
13+
}
14+
15+
provider "port" {
16+
client_id = var.port_client_id
17+
secret = var.port_client_secret
18+
}
19+
20+
variable "port_client_id" {
21+
type = string
22+
description = "Port client ID"
23+
}
24+
25+
variable "port_client_secret" {
26+
type = string
27+
sensitive = true
28+
description = "Port client secret"
29+
}
30+
31+
# Create a service blueprint
32+
resource "port_blueprint" "service" {
33+
title = "Service"
34+
icon = "Microservice"
35+
identifier = "dynamic_filter_example_service"
36+
properties = {
37+
string_props = {
38+
"environment" = {
39+
title = "Environment"
40+
enum = ["development", "staging", "production"]
41+
}
42+
"team" = {
43+
title = "Team"
44+
}
45+
}
46+
}
47+
}
48+
49+
# Create test entities - mix of environments and teams
50+
resource "port_entity" "service_dev_engineering" {
51+
identifier = "service-dev-api"
52+
title = "dev-api-service"
53+
blueprint = port_blueprint.service.identifier
54+
55+
properties = {
56+
string_props = {
57+
"environment" = "development"
58+
"team" = "engineering"
59+
}
60+
}
61+
}
62+
63+
resource "port_entity" "service_staging_platform" {
64+
identifier = "service-staging-web"
65+
title = "staging-web-frontend"
66+
blueprint = port_blueprint.service.identifier
67+
68+
properties = {
69+
string_props = {
70+
"environment" = "staging"
71+
"team" = "platform"
72+
}
73+
}
74+
}
75+
76+
resource "port_entity" "service_prod_engineering" {
77+
identifier = "service-prod-api"
78+
title = "prod-api-service"
79+
blueprint = port_blueprint.service.identifier
80+
81+
properties = {
82+
string_props = {
83+
"environment" = "production"
84+
"team" = "engineering"
85+
}
86+
}
87+
}
88+
89+
resource "port_entity" "service_dev_platform" {
90+
identifier = "service-dev-web"
91+
title = "dev-web-frontend"
92+
blueprint = port_blueprint.service.identifier
93+
94+
properties = {
95+
string_props = {
96+
"environment" = "development"
97+
"team" = "platform"
98+
}
99+
}
100+
}
101+
102+
# Action with dynamic form filtering
103+
# The entity selector filters based on the team selected in the form AND environment
104+
resource "port_action" "select_service" {
105+
title = "Select Service by Team"
106+
identifier = "dynamic_filter_example_select_service"
107+
108+
self_service_trigger = {
109+
operation = "DAY-2"
110+
blueprint_identifier = port_blueprint.service.identifier
111+
112+
order_properties = ["selected_team", "target_service"]
113+
114+
user_properties = {
115+
string_props = {
116+
# First field: Team selector dropdown
117+
selected_team = {
118+
title = "Select Team"
119+
description = "Choose a team to filter services"
120+
enum = ["engineering", "platform"]
121+
}
122+
123+
# Second field: Entity selector filtered by the selected team AND non-prod environment
124+
target_service = {
125+
title = "Target Service"
126+
format = "entity"
127+
blueprint = port_blueprint.service.identifier
128+
depends_on = ["selected_team"]
129+
130+
# Dynamic filtering using JQ expressions
131+
dataset = {
132+
combinator = "and"
133+
rules = [
134+
# Rule 1: Dynamic filter - team matches form selection
135+
# .form.selected_team evaluates to the user's selection at runtime
136+
{
137+
property = "team"
138+
operator = "="
139+
value = {
140+
jq_query = ".form.selected_team"
141+
}
142+
},
143+
# Rule 2: Literal string filter - only non-production environments
144+
# The value is wrapped in quotes to indicate it's a literal string in JQ
145+
{
146+
property = "environment"
147+
operator = "!="
148+
value = {
149+
jq_query = "\"production\""
150+
}
151+
}
152+
]
153+
}
154+
}
155+
}
156+
}
157+
}
158+
159+
webhook_method = {
160+
url = "https://example.com/webhook"
161+
}
162+
}
163+
164+
output "action_identifier" {
165+
value = port_action.select_service.identifier
166+
}

internal/cli/models.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,27 +175,17 @@ func (dv *DatasetValue) UnmarshalJSON(data []byte) error {
175175
return nil
176176
}
177177

178-
// Custom MarshalJSON for DatasetValue to preserve the original format when possible
178+
// Custom MarshalJSON for DatasetValue to always preserve the jqQuery wrapper.
179+
// The Port API expects dataset rule values to be in the format {"jqQuery": "..."}
180+
// for dynamic JQ expression evaluation.
179181
func (dv DatasetValue) MarshalJSON() ([]byte, error) {
180-
181182
if dv.JqQuery == "" {
182183
return []byte("null"), nil
183184
}
184185

185-
if dv.JqQuery == "true" || dv.JqQuery == "false" {
186-
return []byte(dv.JqQuery), nil
187-
}
188-
189-
var numTest float64
190-
if err := json.Unmarshal([]byte(dv.JqQuery), &numTest); err == nil {
191-
return []byte(dv.JqQuery), nil
192-
}
193-
194-
if len(dv.JqQuery) >= 2 && dv.JqQuery[0] == '"' && dv.JqQuery[len(dv.JqQuery)-1] == '"' {
195-
return []byte(dv.JqQuery), nil
196-
}
197-
198-
return json.Marshal(dv.JqQuery)
186+
// Always output the jqQuery wrapper - the API requires this format
187+
// for proper JQ expression evaluation in dataset rules
188+
return json.Marshal(map[string]string{"jqQuery": dv.JqQuery})
199189
}
200190

201191
type (

0 commit comments

Comments
 (0)