Skip to content

Commit e6ef97e

Browse files
authored
Merge pull request #468 from hookdeck/fix/rule-transform-name-or-id
fix(connection): resolve --rule-transform-name as a name or ID
2 parents c6efd9a + a1ec850 commit e6ef97e

11 files changed

Lines changed: 596 additions & 12 deletions

‎KNOWN_ISSUES.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# Known Issues
22

3+
## `--rule-transform-name` with an ID or a mistyped name attaches an empty transformation
4+
5+
**Symptom:** `hookdeck gateway connection create`, `update` or `upsert` with `--rule-transform-name`
6+
set to a transformation ID, or to a name that doesn't exist, succeeds, but every event on the
7+
connection then fails its transformation with a `TypeError`.
8+
9+
**Why:** The CLI always sent the value as a transformation name. When no transformation has that
10+
name, the API creates one, even with no code, so the connection got a new, empty transformation
11+
instead of the one you meant.
12+
13+
**Affected:** v3.1.0 and earlier.
14+
15+
**Recommended fix:** Update to **v3.2.0 or later**. The value is then resolved as an ID or an exact
16+
name, and an unknown value without `--rule-transform-code` is an error.
17+
18+
- npm: `npm install -g hookdeck-cli@latest`
19+
- Homebrew: `brew upgrade hookdeck`
20+
21+
**Workaround (until you update):** Pass the rule as JSON with `--rules` or `--rules-file`,
22+
referencing the transformation by `transformation_id`.
23+
24+
**Cleanup:** If you hit this, you have empty transformations named after an ID (`trs_...`) or a
25+
typo. Find them with `hookdeck gateway transformation list`. Point any connection that uses one at
26+
the right transformation, then delete it with `hookdeck gateway transformation delete <id>`.
27+
28+
**Tracking:** #468
29+
330
## Connection rules built from `--rule-*` flags run in a fixed order, not the order you wrote them
431

532
**Symptom:** A connection created, updated or upserted with `--rule-*` flags runs its rules in a

‎REFERENCE.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,9 @@ hookdeck gateway connection create [flags]
402402
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
403403
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
404404
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
405-
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
406-
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
407-
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
405+
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
406+
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
407+
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
408408
| `--rules` | `string` | JSON string representing the entire rules array |
409409
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
410410
| `--source-allowed-http-methods` | `string` | Comma-separated list of allowed HTTP methods (GET, POST, PUT, PATCH, DELETE) |
@@ -521,9 +521,9 @@ hookdeck gateway connection update <connection-id> [flags]
521521
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
522522
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
523523
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
524-
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
525-
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
526-
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
524+
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
525+
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
526+
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
527527
| `--rules` | `string` | JSON string representing the entire rules array |
528528
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
529529
| `--source-id` | `string` | Update source by ID |
@@ -661,9 +661,9 @@ hookdeck gateway connection upsert <name> [flags]
661661
| `--rule-retry-interval` | `int` | Interval between retries in milliseconds (default "0") |
662662
| `--rule-retry-response-status-codes` | `string` | Comma-separated HTTP status codes to retry on |
663663
| `--rule-retry-strategy` | `string` | Retry strategy (linear, exponential) |
664-
| `--rule-transform-code` | `string` | Transformation code (if creating inline) |
665-
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation |
666-
| `--rule-transform-name` | `string` | Name or ID of the transformation to apply |
664+
| `--rule-transform-code` | `string` | Transformation code. Creates the transformation named by `--rule-transform-name`, or replaces its code if it exists |
665+
| `--rule-transform-env` | `string` | JSON string representing environment variables for transformation. Replaces the env of an existing transformation |
666+
| `--rule-transform-name` | `string` | Name or ID of an existing transformation to apply. With `--rule-transform-code`, an existing transformation's code is replaced, or a new transformation is created with this name |
667667
| `--rules` | `string` | JSON string representing the entire rules array |
668668
| `--rules-file` | `string` | Path to a JSON file containing the rules array |
669669
| `--source-allowed-http-methods` | `string` | Comma-separated list of allowed HTTP methods (GET, POST, PUT, PATCH, DELETE) |

‎pkg/cmd/connection_common.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ func addConnectionRuleFlags(cmd *cobra.Command, f *connectionRuleFlags) {
135135
cmd.Flags().StringVar(&f.RuleFilterQuery, "rule-filter-query", "", "Filter on request query parameters using Hookdeck filter syntax (JSON)")
136136
cmd.Flags().StringVar(&f.RuleFilterPath, "rule-filter-path", "", "Filter on request path using Hookdeck filter syntax (JSON)")
137137

138-
cmd.Flags().StringVar(&f.RuleTransformName, "rule-transform-name", "", "Name or ID of the transformation to apply")
139-
cmd.Flags().StringVar(&f.RuleTransformCode, "rule-transform-code", "", "Transformation code (if creating inline)")
140-
cmd.Flags().StringVar(&f.RuleTransformEnv, "rule-transform-env", "", "JSON string representing environment variables for transformation")
138+
cmd.Flags().StringVar(&f.RuleTransformName, "rule-transform-name", "", "Name or ID of an existing transformation to apply. With --rule-transform-code, an existing transformation's code is replaced, or a new transformation is created with this name")
139+
cmd.Flags().StringVar(&f.RuleTransformCode, "rule-transform-code", "", "Transformation code. Creates the transformation named by --rule-transform-name, or replaces its code if it exists")
140+
cmd.Flags().StringVar(&f.RuleTransformEnv, "rule-transform-env", "", "JSON string representing environment variables for transformation. Replaces the env of an existing transformation")
141141

142142
cmd.Flags().IntVar(&f.RuleDelay, "rule-delay", 0, "Delay in milliseconds")
143143

‎pkg/cmd/connection_create.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,9 @@ func (cc *connectionCreateCmd) runConnectionCreateCmd(cmd *cobra.Command, args [
459459
if err != nil {
460460
return err
461461
}
462+
if err := resolveRuleTransformation(context.Background(), client, &cc.connectionRuleFlags, rules); err != nil {
463+
return err
464+
}
462465
if len(rules) > 0 {
463466
req.Rules = &rules
464467
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/hookdeck/hookdeck-cli/pkg/hookdeck"
10+
)
11+
12+
// The lookup doesn't rely on the name filter on GET /transformations being an
13+
// exact match. It compares names exactly and, in case the filter matches more
14+
// than one page, asks for the largest page the API allows and follows
15+
// pagination.next, up to a bound.
16+
const (
17+
transformationLookupPageLimit = 255
18+
transformationLookupMaxPages = 20
19+
)
20+
21+
// transformationLookup is the subset of the API client used to resolve
22+
// --rule-transform-name.
23+
type transformationLookup interface {
24+
GetTransformation(ctx context.Context, id string) (*hookdeck.Transformation, error)
25+
ListTransformations(ctx context.Context, params map[string]string) (*hookdeck.TransformationListResponse, error)
26+
}
27+
28+
// findTransformationID returns the ID of the transformation whose ID or exact
29+
// name is nameOrID, or "" when there is none.
30+
func findTransformationID(ctx context.Context, client transformationLookup, nameOrID string) (string, error) {
31+
if strings.HasPrefix(nameOrID, "trs_") {
32+
trn, err := client.GetTransformation(ctx, nameOrID)
33+
if err == nil {
34+
return trn.ID, nil
35+
}
36+
if !hookdeck.IsNotFoundError(err) {
37+
return "", fmt.Errorf("failed to look up transformation '%s': %w", nameOrID, err)
38+
}
39+
}
40+
41+
params := map[string]string{
42+
"name": nameOrID,
43+
"limit": strconv.Itoa(transformationLookupPageLimit),
44+
}
45+
for page := 0; page < transformationLookupMaxPages; page++ {
46+
result, err := client.ListTransformations(ctx, params)
47+
if err != nil {
48+
return "", fmt.Errorf("failed to look up transformation '%s': %w", nameOrID, err)
49+
}
50+
for _, trn := range result.Models {
51+
if trn.Name == nameOrID {
52+
return trn.ID, nil
53+
}
54+
}
55+
next := result.Pagination.Next
56+
if next == nil || *next == "" {
57+
return "", nil
58+
}
59+
params["next"] = *next
60+
}
61+
return "", fmt.Errorf("failed to look up transformation '%s': more than %d transformations match the name filter; pass its ID instead",
62+
nameOrID, transformationLookupPageLimit*transformationLookupMaxPages)
63+
}
64+
65+
// resolveRuleTransformation makes --rule-transform-name accept a transformation
66+
// name or ID, as its help text says.
67+
//
68+
// The API matches a transform rule's transformation by name and creates one when
69+
// the name is new, even without code. Sent as a name, an ID or a mistyped name
70+
// therefore created an empty transformation that failed every event. So the value
71+
// is resolved first:
72+
// - found: the rule references it by transformation_id; --rule-transform-code and
73+
// --rule-transform-env, if given, update that transformation
74+
// - not found, with --rule-transform-code: a new transformation is created with
75+
// that name, as before (not allowed for an ID)
76+
// - not found, without --rule-transform-code: an error
77+
//
78+
// Rules from --rules or --rules-file are sent as given.
79+
func resolveRuleTransformation(ctx context.Context, client transformationLookup, f *connectionRuleFlags, rules []hookdeck.Rule) error {
80+
if f.Rules != "" || f.RulesFile != "" {
81+
return nil
82+
}
83+
if f.RuleTransformName == "" {
84+
if f.RuleTransformCode != "" || f.RuleTransformEnv != "" {
85+
return fmt.Errorf("--rule-transform-name is required when using transform rule flags")
86+
}
87+
return nil
88+
}
89+
90+
idx := -1
91+
for i, r := range rules {
92+
if r["type"] == "transform" {
93+
idx = i
94+
break
95+
}
96+
}
97+
if idx == -1 {
98+
return nil
99+
}
100+
101+
nameOrID := f.RuleTransformName
102+
id, err := findTransformationID(ctx, client, nameOrID)
103+
if err != nil {
104+
return err
105+
}
106+
107+
if id == "" {
108+
if strings.HasPrefix(nameOrID, "trs_") {
109+
return fmt.Errorf("transformation '%s' not found. Check the ID with 'hookdeck gateway transformation list'", nameOrID)
110+
}
111+
if f.RuleTransformCode == "" {
112+
return fmt.Errorf("transformation '%s' not found. Check the name with 'hookdeck gateway transformation list', or pass --rule-transform-code to create a new transformation with this name", nameOrID)
113+
}
114+
// New transformation: keep name and code so the API creates it
115+
return nil
116+
}
117+
118+
rule := hookdeck.Rule{"type": "transform", "transformation_id": id}
119+
if existing, ok := rules[idx]["transformation"].(map[string]interface{}); ok {
120+
update := make(map[string]interface{})
121+
for _, key := range []string{"code", "env"} {
122+
if v, ok := existing[key]; ok {
123+
update[key] = v
124+
}
125+
}
126+
if len(update) > 0 {
127+
rule["transformation"] = update
128+
}
129+
}
130+
rules[idx] = rule
131+
return nil
132+
}

0 commit comments

Comments
 (0)