Skip to content

Commit 88e19b7

Browse files
authored
Merge pull request #161 from hookdeck/2025-07-01
feat: change connection rules schema from set to list & bump hookdeck api version to 2025-07-01
2 parents df87e1b + 19ef81c commit 88e19b7

File tree

17 files changed

+750
-177
lines changed

17 files changed

+750
-177
lines changed

docs/data-sources/connection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ data "hookdeck_connection" "example" {
3333
- `disabled_at` (String) Date the connection was disabled
3434
- `name` (String) A unique, human-friendly name for the connection
3535
- `paused_at` (String) Date the connection was paused
36-
- `rules` (Attributes Set) (see [below for nested schema](#nestedatt--rules))
36+
- `rules` (Attributes List) (see [below for nested schema](#nestedatt--rules))
3737
- `source_id` (String) ID of a source to bind to the connection
3838
- `team_id` (String) ID of the workspace
3939
- `updated_at` (String) Date the connection was last updated

docs/guides/rules.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,37 @@ description: "Connection Rules"
66
# Hookdeck Rules
77

88
A rule is a piece of instructional logic that dictates the behavior of events
9-
routed through a connection. There are 4 types of rules in Hookdeck:
9+
routed through a connection.
10+
11+
### Rule Execution Order
12+
13+
The order of rules in the `rules` list matters. `filter_rule` and `transform_rule` blocks are executed sequentially in the order they are defined. This allows you to create powerful workflows, such as filtering a request *before* transforming it to avoid unnecessary processing.
14+
15+
Other rule types, like `retry_rule` and `delay_rule`, are not affected by ordering, as their execution is not sequential.
16+
17+
For example, you can define a `filter_rule` to run before a `transform_rule`:
18+
19+
```hcl
20+
resource "hookdeck_connection" "my_connection" {
21+
# ...
22+
rules = [
23+
{
24+
filter_rule = {
25+
body = {
26+
json = jsonencode({ "status" = "active" })
27+
}
28+
}
29+
},
30+
{
31+
transform_rule = {
32+
transformation_id = hookdeck_transformation.my_transformation.id
33+
}
34+
}
35+
]
36+
}
37+
```
38+
39+
There are 4 types of rules in Hookdeck:
1040

1141
## Retry rule
1242

docs/guides/v1-to-v2-migration.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
page_title: "Migrating from v1.x to v2.x"
3+
description: "How to migrate from v1.x to v2.x of the Hookdeck Terraform Provider"
4+
---
5+
6+
The Hookdeck API version `2025-07-01` introduces a [breaking change](https://hookdeck.com/docs/api#2025-07-01), [Transform](https://hookdeck.com/docs/transformations) and [Filter](https://hookdeck.com/docs/filters) rules in a connection are now **executed in the order defined**. Previously, the execution order was fixed: **Transform rules always ran before Filter rules**.
7+
8+
Hookdeck has automatically migrated all existing rules to match the previous behavior (Transform before Filter). However, your **Terraform configuration may now show diffs** if it defines rules in a different order than what is stored remotely.
9+
10+
This guide walks through how to adjust your configuration to match the new behavior.
11+
12+
## Summary of Change
13+
14+
- **What changed**: The `rules` list in a `hookdeck_connection` is now **ordered**. `transform_rule` and `filter_rule` blocks are executed in the order they appear.
15+
- **Which rules are affected**: Only `transform_rule` and `filter_rule` blocks respect the order. Other rule blocks (e.g., `retry_rule`, `delay_rule`) are not affected as their execution order is not important.
16+
- **Default behavior**: Existing Hookdeck connections have been migrated so that `transform_rule` blocks appear before `filter_rule` blocks in the `rules` list—preserving legacy behavior.
17+
- **Terraform implications**: If your configuration has a `filter_rule` before a `transform_rule`, Terraform will detect this as a change.
18+
19+
## What You Should Do
20+
21+
Update your Terraform `rules` definition to ensure `transform_rule` blocks come **before** `filter_rule` blocks, unless you intentionally want the new ordering behavior.
22+
23+
### Example
24+
25+
The `v1.x` provider introduced the `rules` attribute with the modern syntax for `filter_rule` and `transform_rule`. The breaking change in `v2.x` is that the *order* of these rules now matters.
26+
27+
For example, if your configuration defined a `filter_rule` before a `transform_rule`:
28+
29+
```hcl
30+
resource "hookdeck_transformation" "my_transformation" {
31+
name = "my-transformation"
32+
code = file("${path.module}/transformation.js")
33+
}
34+
35+
resource "hookdeck_connection" "my_connection" {
36+
# ... other attributes like source_id and destination_id
37+
38+
rules = [
39+
{
40+
// This filter rule runs first in the list...
41+
filter_rule = {
42+
body = {
43+
// Using jsonencode is recommended for readability
44+
json = jsonencode({
45+
status = "active"
46+
})
47+
}
48+
}
49+
},
50+
{
51+
// ...followed by this transform rule.
52+
transform_rule = {
53+
transformation_id = hookdeck_transformation.my_transformation.id
54+
}
55+
}
56+
]
57+
}
58+
```
59+
60+
Because Hookdeck automatically migrated existing connections to have `transform` rules run before `filter` rules, the above configuration will produce a `terraform plan` diff.
61+
62+
To align your configuration with the migrated state and avoid the diff, you must reorder the rules in your file:
63+
64+
```hcl
65+
resource "hookdeck_transformation" "my_transformation" {
66+
name = "my-transformation"
67+
code = file("${path.module}/transformation.js")
68+
}
69+
70+
resource "hookdeck_connection" "my_connection" {
71+
# ... other attributes like source_id and destination_id
72+
73+
rules = [
74+
{
75+
// The transform rule is now first, matching the legacy behavior.
76+
transform_rule = {
77+
transformation_id = hookdeck_transformation.my_transformation.id
78+
}
79+
},
80+
{
81+
// The filter rule is now second.
82+
filter_rule = {
83+
body = {
84+
json = jsonencode({
85+
status = "active"
86+
})
87+
}
88+
}
89+
}
90+
]
91+
}
92+
```
93+
94+
This will preserve the same behavior as before the API update and resolve any `terraform plan` diffs.
95+
96+
## Optional: Leverage the New Behavior
97+
98+
This update also unlocks new use cases:
99+
100+
- Run a **filter before a transformation** to skip unnecessary processing.
101+
- Use a transformation **after** a filter to clean up headers or payloads before delivery.
102+
103+
If you're intentionally using the new ordering capability, no migration is needed—just ensure your rule order in Terraform matches your intended logic.
104+
105+
## Final Notes
106+
107+
- The Hookdeck provider does **not** modify rule order—your configuration must match your intended logic.
108+
- If you see diffs after upgrading, they are likely due to mismatched rule order and can be resolved by reordering locally.
109+
- The provider will output a warning message if it detects that your existing rule definitions have a filter rule before a transformation rule.
110+
111+
For questions or help migrating, please [raise an issue on the Hookdeck Terraform Provider GitHub repo](https://github.com/hookdeck/terraform-provider-hookdeck/issues).

docs/resources/connection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ resource "hookdeck_connection" "connection_example" {
7171
- `description` (String) Description for the connection
7272
- `disabled_at` (String) Date the connection was disabled
7373
- `name` (String) A unique, human-friendly name for the connection
74-
- `rules` (Attributes Set) (see [below for nested schema](#nestedatt--rules))
74+
- `rules` (Attributes List) (see [below for nested schema](#nestedatt--rules))
7575

7676
### Read-Only
7777

examples/full/main.tf

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,25 @@ resource "hookdeck_destination" "aws_destination" {
115115
})
116116
}
117117

118+
resource "hookdeck_transformation" "example_transformation" {
119+
name = "example_transformation"
120+
code = <<EOT
121+
addHandler("transform", (request, context) => {
122+
request.headers["example-header"] = "Hello World";
123+
return request;
124+
});
125+
EOT
126+
}
127+
118128
resource "hookdeck_connection" "first_connection" {
119129
source_id = hookdeck_source.first_source.id
120130
destination_id = hookdeck_destination.first_destination.id
121131
rules = [
132+
{
133+
transform_rule = {
134+
transformation_id = hookdeck_transformation.example_transformation.id
135+
}
136+
},
122137
{
123138
filter_rule = {
124139
headers = {
@@ -127,7 +142,7 @@ resource "hookdeck_connection" "first_connection" {
127142
})
128143
}
129144
}
130-
}
145+
},
131146
]
132147
}
133148

internal/provider/connection/resource.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func (r *connectionResource) Metadata(_ context.Context, req resource.MetadataRe
3535
// Schema returns the resource schema.
3636
func (r *connectionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
3737
resp.Schema = schema.Schema{
38+
Version: 1,
3839
MarkdownDescription: "Connection Resource",
3940
Attributes: schemaAttributes(),
4041
}
Lines changed: 1 addition & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,9 @@
11
package connection
22

33
import (
4-
"terraform-provider-hookdeck/internal/validators"
5-
6-
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
74
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
8-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
9-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
10-
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
115
)
126

137
func schemaAttributes() map[string]schema.Attribute {
14-
filterRulePropertySchema := schema.SingleNestedAttribute{
15-
Optional: true,
16-
Attributes: map[string]schema.Attribute{
17-
"boolean": schema.BoolAttribute{
18-
Optional: true,
19-
},
20-
"json": schema.StringAttribute{
21-
Optional: true,
22-
Description: `Stringied JSON using our filter syntax to filter on request headers`,
23-
},
24-
"number": schema.NumberAttribute{
25-
Optional: true,
26-
},
27-
"string": schema.StringAttribute{
28-
Optional: true,
29-
},
30-
},
31-
Validators: []validator.Object{
32-
validators.ExactlyOneChild(),
33-
},
34-
}
35-
36-
return map[string]schema.Attribute{
37-
"created_at": schema.StringAttribute{
38-
Computed: true,
39-
Validators: []validator.String{
40-
validators.IsRFC3339(),
41-
},
42-
PlanModifiers: []planmodifier.String{
43-
stringplanmodifier.UseStateForUnknown(),
44-
},
45-
Description: "Date the connection was created",
46-
},
47-
"description": schema.StringAttribute{
48-
Optional: true,
49-
PlanModifiers: []planmodifier.String{
50-
stringplanmodifier.UseStateForUnknown(),
51-
},
52-
Description: "Description for the connection",
53-
},
54-
"destination_id": schema.StringAttribute{
55-
Required: true,
56-
PlanModifiers: []planmodifier.String{
57-
stringplanmodifier.RequiresReplace(),
58-
},
59-
Description: "ID of a destination to bind to the connection",
60-
},
61-
"disabled_at": schema.StringAttribute{
62-
Computed: true,
63-
Optional: true,
64-
Validators: []validator.String{
65-
validators.IsRFC3339(),
66-
},
67-
Description: "Date the connection was disabled",
68-
},
69-
"id": schema.StringAttribute{
70-
Computed: true,
71-
Description: `ID of the connection`,
72-
PlanModifiers: []planmodifier.String{
73-
stringplanmodifier.UseStateForUnknown(),
74-
},
75-
},
76-
"name": schema.StringAttribute{
77-
Optional: true,
78-
PlanModifiers: []planmodifier.String{
79-
stringplanmodifier.UseStateForUnknown(),
80-
},
81-
Description: `A unique, human-friendly name for the connection`,
82-
},
83-
"paused_at": schema.StringAttribute{
84-
Computed: true,
85-
Validators: []validator.String{
86-
validators.IsRFC3339(),
87-
},
88-
Description: "Date the connection was paused",
89-
},
90-
"rules": schema.SetNestedAttribute{
91-
Optional: true,
92-
NestedObject: schema.NestedAttributeObject{
93-
Attributes: map[string]schema.Attribute{
94-
"delay_rule": schema.SingleNestedAttribute{
95-
Optional: true,
96-
Attributes: map[string]schema.Attribute{
97-
"delay": schema.Int64Attribute{
98-
Required: true,
99-
Description: `Delay to introduce in MS`,
100-
},
101-
},
102-
},
103-
"filter_rule": schema.SingleNestedAttribute{
104-
Optional: true,
105-
Attributes: map[string]schema.Attribute{
106-
"body": filterRulePropertySchema,
107-
"headers": filterRulePropertySchema,
108-
"path": filterRulePropertySchema,
109-
"query": filterRulePropertySchema,
110-
},
111-
Validators: []validator.Object{
112-
validators.AtLeastOneChild(),
113-
},
114-
},
115-
"retry_rule": schema.SingleNestedAttribute{
116-
Optional: true,
117-
Attributes: map[string]schema.Attribute{
118-
"count": schema.Int64Attribute{
119-
Optional: true,
120-
Description: `Maximum number of retries to attempt`,
121-
},
122-
"interval": schema.Int64Attribute{
123-
Optional: true,
124-
Description: `Time in MS between each retry`,
125-
},
126-
"strategy": schema.StringAttribute{
127-
Required: true,
128-
Validators: []validator.String{
129-
stringvalidator.OneOf(
130-
"linear",
131-
"exponential",
132-
),
133-
},
134-
MarkdownDescription: `must be one of ["linear", "exponential"]` + "\n" +
135-
`Algorithm to use when calculating delay between retries`,
136-
},
137-
},
138-
},
139-
"transform_rule": schema.SingleNestedAttribute{
140-
Optional: true,
141-
Attributes: map[string]schema.Attribute{
142-
"transformation_id": schema.StringAttribute{
143-
Required: true,
144-
Description: `ID of the attached transformation object.`,
145-
},
146-
},
147-
},
148-
},
149-
Validators: []validator.Object{
150-
validators.ExactlyOneChild(),
151-
},
152-
},
153-
},
154-
"source_id": schema.StringAttribute{
155-
Required: true,
156-
PlanModifiers: []planmodifier.String{
157-
stringplanmodifier.RequiresReplace(),
158-
},
159-
Description: `ID of a source to bind to the connection`,
160-
},
161-
"team_id": schema.StringAttribute{
162-
Computed: true,
163-
PlanModifiers: []planmodifier.String{
164-
stringplanmodifier.UseStateForUnknown(),
165-
},
166-
Description: `ID of the workspace`,
167-
},
168-
"updated_at": schema.StringAttribute{
169-
Computed: true,
170-
Validators: []validator.String{
171-
validators.IsRFC3339(),
172-
},
173-
Description: `Date the connection was last updated`,
174-
},
175-
}
8+
return schemaAttributesV1()
1769
}

0 commit comments

Comments
 (0)