fix(api): normalize destination topics on write#994
Merged
alexluong merged 2 commits intoJul 14, 2026
Conversation
Add Topics.Normalize() and call it after Validate in the destination create and update handlers. It dedupes exact duplicate entries and folds an entry that a sibling wildcard pattern strictly covers (["user.*","user.created"] -> ["user.*"]), while leaving mutually-non- covering pattern pairs (["*.created","user.*"]) and ["*"] untouched. Preserves first-seen order so API responses stay stable. Normalization only removes entries already covered by a retained entry, so MatchTopic (delivery) results are unchanged. Closes hookdeck#990
alexluong
approved these changes
Jul 13, 2026
alexluong
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for the PR, LGTM!
One small comment is can we add some tests at the handler level where we send POST/PUT/PATCH request with different topics input and confirm that the topic list is normalized?
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Topics.Validatechecks each entry independently, so a destination's storedtopic set can contain redundant entries that are accepted verbatim:
["user.created", "user.created"]["user.*", "user.created"]["*.created", "user.created", "user.*"]Delivery is unaffected (topic matching is any-entry-wins), but the persisted
array is ambiguous as data: duplicates echo back on every read (API/SDK/portal
and
tenant.subscriptions.updatedpayloads), and there is no defined editsemantics for a redundant set. Since #912 (
TOPICS_ALLOW_WILDCARDS) the portalitself can create these mixed states, so they occur organically.
This adds
Topics.Normalize()and calls it afterValidatein the destinationcreate and update handlers:
(
["user.*", "user.created"]->["user.*"]), where "strictly covers" meansthe pattern covers the entry but the entry does not cover the pattern;
(
["*.created", "user.*"]is unchanged);["*"]and already-minimal sets untouched;Because it only removes entries already covered by a retained entry,
MatchTopic(delivery) results are identical before and after. A create/update with
["user.*", "user.created"]now stores and returns["user.*"].Why this matters
This implements the "normalize on write" direction from the issue author
(@alexluong): "dedupe exact duplicates and fold entries covered by a sibling
pattern (
["user.*", "user.created"]->["user.*"])", reaffirmed as "ifuser.*then should not haveuser.created". Normalizing at the write boundarymakes the persisted topic set unambiguous, so every downstream reader (API, SDK,
portal, subscription-update events) sees a single canonical representation. It
also gives the portal topic editor a well-defined data model to build on.
Out of scope (deliberately)
TopicPicker.tsxhasWildcardonChangefixes. That is a separatefrontend change requiring the portal running to verify; this backend
normalization is the prerequisite that makes the persisted data unambiguous.
*.created("wildcard as last param",raised by @alexbouchardd). That is a validation-tightening change that could
reject currently-valid stored data, a distinct decision from normalization -
left for a maintainer as a known follow-up.
Per the issue's own note, pattern-vs-pattern overlap where neither covers the
other (
["*.created", "user.*"]) is intentionally left intact.Tests
TestDestinationTopics_Normalizetable test covering dedupe, fold,mutually-non-covering patterns,
["*"], already-minimal sets, and orderpreservation.
TestDestinationTopics_Validateunchanged.go build ./...,go test ./internal/models/... ./internal/apirouter/...,and
go veton the changed packages all pass.Closes #990