Skip to content

Commit 8ab6cac

Browse files
committed
feat: Add connection upsert command with full control
- Implement idempotent upsert command (create or update by name) - Add dry-run support for previewing changes - Remove deprecated update command in favor of upsert - Add 10 comprehensive acceptance tests with input/output validation - Fix API validation for partial updates - Change name from flag to positional argument per design spec - Update documentation and examples All 21 connection tests passing
1 parent 8acf8d3 commit 8ab6cac

File tree

8 files changed

+1044
-267
lines changed

8 files changed

+1044
-267
lines changed

.plans/connection-upsert-design.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Connection Upsert Command Design
2+
3+
## 1. Command Overview
4+
5+
### Purpose and Use Cases
6+
The `hookdeck connection upsert` command provides an idempotent mechanism for creating or updating connections. It is designed for users who manage their webhook infrastructure as code, enabling them to define the desired state of a connection without worrying about its current state.
7+
8+
- **Primary Use Case:** Declarative connection management in CI/CD pipelines or local scripts.
9+
- **Secondary Use Case:** A convenient way to make partial updates to a connection without fetching its full state.
10+
11+
### Comparison with `create`
12+
- `hookdeck connection create`: Explicitly creates a new connection. Fails if a connection with the same name already exists. Use this when you intend to create a new resource and want to avoid accidental overwrites.
13+
- `hookdeck connection upsert`: Creates a connection if it doesn't exist by name, or updates it if it does. Use this for idempotent operations where the end state is what matters.
14+
15+
## 2. Command Signature
16+
17+
The command will use the connection `name` as a required positional argument, making it distinct and easy to use.
18+
19+
```bash
20+
hookdeck connection upsert <name> [flags]
21+
```
22+
23+
## 3. Flag Definitions
24+
25+
The `upsert` command will reuse all flags from the `connection create` command to ensure full control over connection properties. A new `--dry-run` flag will be added to preview changes.
26+
27+
- **All flags will be optional.** When updating, only the provided flags will modify the connection's properties.
28+
- **`--dry-run` (boolean):** If present, the command will not perform the final `PUT` request. Instead, it will output a summary of the intended action (create or update) and the data that would be sent.
29+
30+
## 4. Dry-Run Behavior
31+
32+
The `--dry-run` flag is critical for safely managing infrastructure.
33+
34+
### Implementation
35+
1. The command will first make a `GET /connections?name=<name>` API call to check if a connection with the given name exists.
36+
2. Based on the result, it will determine whether the operation is a **CREATE** or an **UPDATE**.
37+
3. It will construct the full request payload just as it would for a real operation.
38+
4. It will output a summary indicating the action and the payload.
39+
40+
### Output Format
41+
The dry-run output will be a human-readable text summary, with an option for JSON output for scripting.
42+
43+
**Text Summary Example (Create):**
44+
```
45+
-- Dry Run: CREATE --
46+
A new connection named 'my-connection' will be created with the following properties:
47+
- Source: my-source (inline)
48+
- Destination: my-destination (inline)
49+
- Rules:
50+
- Retry: exponential, 3 attempts
51+
```
52+
53+
**Text Summary Example (Update):**
54+
```
55+
-- Dry Run: UPDATE --
56+
Connection 'my-connection' (conn_123) will be updated with the following changes:
57+
- Description: "New description"
58+
- Rules: (ruleset will be replaced)
59+
- Filter: body contains '{"$.type":"payment"}'
60+
```
61+
62+
**JSON Output (`--output json`):**
63+
```json
64+
{
65+
"action": "CREATE",
66+
"connection_name": "my-connection",
67+
"payload": {
68+
"name": "my-connection",
69+
"source": { "...etc" },
70+
"destination": { "...etc" }
71+
}
72+
}
73+
```
74+
75+
## 5. Behavior Specification
76+
77+
- **When connection doesn't exist:** The command behaves like `create`. It will create a new connection using the provided flags. `source` and `destination` (either by ID or inline) will be required in this case.
78+
- **When connection exists:** The command will perform an update. Only the properties corresponding to the provided flags will be changed.
79+
- **When no properties provided:** If the connection exists and no flags are provided to modify it, the command will be a **no-op**. It will print a message indicating that no changes are needed. This is not an error.
80+
- **Rule Handling:** If *any* `--rule-*` flag or `--rules-file`/`--rules` is provided, the entire existing ruleset on the connection will be **replaced** with the new rules defined in the command. This ensures declarative and idempotent rule management.
81+
82+
## 6. Validation Rules
83+
84+
Validation logic must accommodate both create and update scenarios.
85+
86+
1. **Initial Check:** The command will first check if the connection exists via a `GET` request.
87+
2. **Create Scenario Validation:** If the connection does not exist:
88+
- `source` (via `--source-id` or inline flags) is required.
89+
- `destination` (via `--destination-id` or inline flags) is required.
90+
- All other validation from `connection create` applies.
91+
3. **Update Scenario Validation:** If the connection exists:
92+
- All flags are optional.
93+
- Validation will only be performed for the flags that are provided.
94+
95+
This approach avoids complex client-side logic and leverages the API for validation where appropriate, while providing helpful early feedback to the user.
96+
97+
## 7. Usage Examples
98+
99+
```bash
100+
# Create a new connection if 'my-connection' does not exist
101+
hookdeck connection upsert my-connection \
102+
--source-name "my-source" --source-type STRIPE \
103+
--destination-name "my-api" --destination-type HTTP --destination-url "https://example.com"
104+
105+
# Update the rules for an existing connection, leaving other properties untouched
106+
hookdeck connection upsert my-connection \
107+
--rule-retry-strategy linear --rule-retry-count 5
108+
109+
# Preview an update with --dry-run
110+
hookdeck connection upsert my-connection \
111+
--description "A new description" --dry-run
112+
113+
# No-op: connection exists, no flags provided
114+
hookdeck connection upsert my-connection
115+
```
116+
117+
## 8. Implementation Guidance
118+
119+
- **Shared Code:** Create a shared `internal/connection` package or a `connection_shared.go` file to house common logic for flag definitions, payload construction, and rule parsing, to be used by both `create` and `upsert` commands.
120+
- **API Client:** Add a new `UpsertConnection` method to the API client that makes a `PUT /connections` call. The method should accept the connection name and the upsert payload.
121+
- **Dry-Run Logic:** The core logic for the dry run will live in the `upsert` command's `RunE` function. It will perform the initial `GET` request and then branch to either display the dry-run output or call the `UpsertConnection` method.
122+
- **Removing `update` command:** Delete `pkg/cmd/connection_update.go` and remove its registration from the parent `connection` command.
123+
124+
## 9. Migration Strategy
125+
126+
- **Remove `update` command:** The `connection update` command will be completely removed.
127+
- **Documentation:** Update `REFERENCE.md` and all other documentation to replace `update` with `upsert`, providing clear examples.
128+
- **CHANGELOG:** Add an entry under "Breaking Changes" announcing the replacement of `connection update` with `connection upsert` and explaining the benefits.
129+
- **User Communication:** Announce the change in release notes, highlighting the new idempotent capabilities and `--dry-run` support.
130+
131+
## 10. Testing Strategy
132+
133+
- **Create Behavior:** Write a test case where the connection name does not exist, and verify that it is created correctly.
134+
- **Update Behavior:** Test that when a connection exists, providing a subset of flags only updates those properties.
135+
- **Idempotency:** Run the same `upsert` command twice and verify that the state is the same after both runs and the second run reports a no-op.
136+
- **Dry-Run Accuracy:** For both create and update scenarios, test that the `--dry-run` output accurately reflects the action and payload.
137+
- **Rule Replacement:** Test that providing rule flags completely replaces the existing ruleset on a connection.

REFERENCE.md

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,18 +1070,45 @@ hookdeck connection create --name "filtered-webhooks" \
10701070
--filter-body "type=invoice.payment_succeeded,invoice.payment_failed"
10711071
```
10721072

1073-
### Update a connection
1073+
### Create or update a connection (upsert) ✅
1074+
1075+
The `upsert` command provides idempotent create-or-update behavior using the connection name as a unique identifier. This is the recommended way to manage connections declaratively.
1076+
10741077
```bash
1075-
# Update connection name
1076-
hookdeck connection update <connection-id> --name "new-name"
1078+
# Create a new connection (when it doesn't exist)
1079+
hookdeck connection upsert "my-connection" \
1080+
--source-name "stripe-prod" \
1081+
--source-type STRIPE \
1082+
--destination-name "my-api" \
1083+
--destination-type HTTP \
1084+
--destination-url "https://api.example.com/webhooks"
10771085

1078-
# Update description
1079-
hookdeck connection update <connection-id> --description "Updated description"
1086+
# Update an existing connection (only updates specified properties)
1087+
hookdeck connection upsert "my-connection" \
1088+
--description "Updated description"
1089+
1090+
# Preview changes without applying them
1091+
hookdeck connection upsert "my-connection" \
1092+
--description "New description" \
1093+
--dry-run
10801094

1081-
# Update both
1082-
hookdeck connection update <connection-id> --name "new-name" --description "Updated description"
1095+
# Update with rules
1096+
hookdeck connection upsert "my-connection" \
1097+
--rule-retry-strategy linear \
1098+
--rule-retry-count 5
10831099
```
10841100

1101+
**Key Features:**
1102+
- **Idempotent**: Safe to run multiple times with the same arguments
1103+
- **Partial updates**: Only updates properties explicitly provided
1104+
- **Dry-run support**: Use `--dry-run` to preview changes before applying
1105+
- **Create or update**: Automatically detects if connection exists by name
1106+
1107+
**When to use:**
1108+
- CI/CD pipelines and automation
1109+
- Infrastructure-as-code workflows
1110+
- When you want to ensure a specific configuration state
1111+
10851112
### Delete a connection
10861113
```bash
10871114
# Delete connection (with confirmation)

pkg/cmd/connection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ existing resources.`,
2525
}
2626

2727
cc.cmd.AddCommand(newConnectionCreateCmd().cmd)
28+
cc.cmd.AddCommand(newConnectionUpsertCmd().cmd)
2829
cc.cmd.AddCommand(newConnectionListCmd().cmd)
2930
cc.cmd.AddCommand(newConnectionGetCmd().cmd)
30-
cc.cmd.AddCommand(newConnectionUpdateCmd().cmd)
3131
cc.cmd.AddCommand(newConnectionDeleteCmd().cmd)
3232
cc.cmd.AddCommand(newConnectionEnableCmd().cmd)
3333
cc.cmd.AddCommand(newConnectionDisableCmd().cmd)

pkg/cmd/connection_update.go

Lines changed: 0 additions & 108 deletions
This file was deleted.

0 commit comments

Comments
 (0)