feat(alerts): model and schema groundwork for multiple notification channels - #2845
Conversation
…hannels zAlertChannels (1..10 entries), MAX_ALERT_CHANNELS, and a shared cross-field rule (exactly one of channel/channels, no duplicates) that both API layers will chain into their alert input schemas. channel becomes optional on AlertBaseObjectSchema; single-channel payloads remain valid unchanged. The Mongoose model gains a canonical channels array and getAlertChannels resolves either document vintage.
Alert responses carry both the canonical channels array and the legacy channel mirror, so a read-modify-write client echoes both back on PUT. A strict exactly-one rule would 400 every GET-then-PUT caller. Accept both when channel equals channels[0], and reject only a genuine disagreement so no client has to guess which field won.
Tile alerts embedded in a saved chart config validate through SavedChartConfigSchema, not the API's alertSchema, so making `channel` optional let a tile alert be saved through the dashboards endpoint with no notification target at all -- it would fire and notify nobody. Apply the channel rule to those unions too. Only the channel rule is applied, not the schedule/threshold refinements, so existing saved dashboards that never passed those keep parsing.
🦋 Changeset detectedLatest commit: bffb953 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds shared multi-channel alert schemas and persists a canonical
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported plural-only persistence issue is addressed by normalizing the input and writing both the canonical channel list and singular compatibility mirror.
|
| Filename | Overview |
|---|---|
| packages/common-utils/src/types.ts | Adds bounded plural-channel schemas and shared validation across API and embedded tile-alert contracts. |
| packages/api/src/controllers/alerts.ts | Normalizes singular and plural inputs, validates every webhook, and persists both canonical channels and the compatibility mirror. |
| packages/api/src/models/alert.ts | Adds plural-channel persistence and a compatibility resolver for old and new alert documents. |
| packages/api/src/routers/api/tests/dashboard.int.test.ts | Verifies that plural-only tile alerts persist both the full channel list and legacy first-channel mirror. |
| packages/api/src/utils/zod.ts | Applies the shared channel-selection rule to the internal alert input schema. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Input[Alert input: channel and/or channels] --> Validate[Shared cross-field validation]
Validate --> Resolve[getAlertChannels]
Resolve --> Persist[(Alert document)]
Persist --> Canonical[channels: all targets]
Persist --> Mirror[channel: first target]
Mirror --> Legacy[Legacy task readers]
Reviews (2): Last reviewed commit: "fix(alerts): make the write path underst..." | Re-trigger Greptile
Deep Review✅ No critical issues found. No P0/P1: the write path is internally consistent ( 🟡 P2 -- recommended
🔵 P3 nitpicks (5)
Reviewers (12): correctness, security, adversarial, testing, maintainability, api-contract, data-migrations, reliability, kieran-typescript, project-standards, agent-native, learnings-researcher. Testing gaps:
|
Review found this PR was internally inconsistent: it made `channel` optional and taught the tile schema to accept `channels`, but no writer understood the new field. A channels-only tile alert saved through the dashboards endpoint passed validation and then persisted with nothing to notify, because makeAlert only ever wrote `channel`. Move the write path in with the schema that enables it: alertSchema accepts channel and/or channels, validateAlertInput checks every webhook belongs to the team in one query, and makeAlert persists the canonical channels array with `channel` mirrored to channels[0]. Tests drive the selection rule through a real AlertSchema parse rather than only a hand-built RefinementCtx, and cover the dashboards path both ways: a channels-only tile alert persists a resolvable target, and one with no channel at all is rejected.
🟡 Tier 3 — StandardIntroduces new logic, modifies core functionality, or touches areas with non-trivial risk. Why this tier:
Additional context: touches background tasks or the delivery pipeline lightly (2 lines, under the 30-line bar for Tier 4) Review process: Full human review — logic, architecture, edge cases. Stats
|
E2E Test Results✅ All tests passed • 274 passed • 1 skipped • 875s
Tests ran across 4 shards in parallel. |
Adds the shared Zod schemas and the Mongoose field that let an alert hold several notification channels instead of one. Nothing reads the new field yet.
What changed
zAlertChannels(1 toMAX_ALERT_CHANNELS, currently 10) and a shared cross-field rule that both API layers chain into their alert input schemas.channelbecomes optional onAlertBaseObjectSchema, with the new rule enforcing that an alert still has a target.The Mongoose model gains a canonical
channelsarray, andgetAlertChannelsresolves an alert's targets regardless of when the document was written.Key decisions
channelsis canonical,channelis kept and mirrored. Storing both means a task runner from before this change still notifies the first target during a rolling upgrade, and a downgrade doesn't strand alerts. No data migration:getAlertChannelshandles both vintages, anddefault: undefinedstops Mongoose materialising an empty array on old documents.Both fields may be sent together when they agree. The rule started as a strict exactly-one-of, but responses carry both fields, so any read-modify-write client would echo both back and get a 400 — the API's own response would not have been a valid request body. Sending both is now accepted when
channelmatcheschannels[0], and a genuine disagreement is still rejected rather than resolved by a silent precedence rule.Only the channel rule is applied to tile alerts. Tile alerts embedded in a saved chart config validate through
SavedChartConfigSchemarather than the API'salertSchema, so the rule had to be attached there too. It deliberately does not pull in the schedule and threshold refinements, which have never run on that path — adding them could reject dashboards that parse today.Impact
Single-channel payloads remain valid unchanged.
channelgoing optional required one defensive fix in the app (alert.channel?.type), where the helper already handled a missing value.Implementation detail
getAlertChannelsprefers a non-emptychannels, falls back tochannelwhen its type is non-null, and otherwise returns empty.The tile-alert rule is pinned by a test that fails without it:
SavedChartConfigSchemarejects a tile alert carrying neither field, and accepts both the plural and legacy-singular forms.Verification: 1693 common-utils unit tests, 669 API unit tests, 265 dashboard integration tests.