feat(services): migrate maintenance domain (PR 2/N)#2103
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 3 Skipped Deployments
|
There was a problem hiding this comment.
5 issues found across 17 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/server/src/routes/rpc/services/maintenance/index.ts">
<violation number="1" location="apps/server/src/routes/rpc/services/maintenance/index.ts:32">
P2: Invalid page component IDs are mapped to `invalidDateFormatError`, producing the wrong error reason/message for this field.</violation>
</file>
<file name="packages/services/src/maintenance/list.ts">
<violation number="1" location="packages/services/src/maintenance/list.ts:98">
P1: `listMaintenances` introduces a per-row enrichment pattern that causes 2 additional queries per item (N+1 behavior). Batch-load component relations for all returned maintenance IDs instead of querying per record.</violation>
</file>
<file name="packages/api/src/router/maintenance.ts">
<violation number="1" location="packages/api/src/router/maintenance.ts:20">
P2: `delete` is no longer idempotent for missing records; it now throws `NOT_FOUND` instead of returning an empty array.</violation>
<violation number="2" location="packages/api/src/router/maintenance.ts:53">
P1: `list` now combines a `10_000` sentinel limit with an N+1 enrichment path, which can generate extremely high query volume.</violation>
</file>
<file name="packages/services/src/maintenance/internal.ts">
<violation number="1" location="packages/services/src/maintenance/internal.ts:35">
P2: `pageComponentIds` are not deduplicated before association inserts, so duplicate IDs can cause composite primary-key violations during create/update.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
b1eb184 to
e70473b
Compare
Review response (Cubic findings)Fixes pushed across the stack — status-report in the parent PR (#2101), maintenance here after a rebase. Summary: ✅ 1. N+1 in
|
3dfbbb8 to
db59d2f
Compare
e70473b to
070ffab
Compare
Second domain migration, stacked on the status-report PR. Same shape as PR 1 — every write path and read path for `maintenance` now goes through `@openstatus/services/maintenance`; tRPC / Connect / Slack are thin adapters. ## Services (`packages/services/src/maintenance/`) - `createMaintenance`, `updateMaintenance`, `deleteMaintenance`, `listMaintenances`, `getMaintenance`, `notifyMaintenance`. - Date range validated at the Zod refine + again inside `updateMaintenance` when partial updates could cross the invariant. - All mutations inside `withTransaction`, emit `emitAudit` before returning. - Internal helpers duplicated from status-report (`validatePageComponentIds`, `updatePageComponentAssociations`, `getMaintenanceInWorkspace`, `getPageComponentIdsForMaintenance`, `getPageComponentsForMaintenance`). A third consumer should trigger the shared-helper extraction. ## Surfaces - **tRPC** (`packages/api/src/router/maintenance.ts`): all four procedures (`delete` / `list` / `new` / `update`) now call services. `emailRouter.sendMaintenance` becomes a thin wrapper over `notifyMaintenance`. - **Connect RPC** (`apps/server/src/routes/rpc/services/maintenance/`): handler rewritten as proto → parse → service → convert. External contract preserved. - **Slack** (`interactions.ts`): `createMaintenance` branch now calls services. Also extracts `getPageUrl`/`getReportUrl` into `apps/server/src/routes/slack/page-urls.ts` — pure transport-layer URL formatting, left on db directly but isolated so `interactions.ts` itself becomes db-free. ## Enforcement - Biome `noRestrictedImports` override now includes: `packages/api/src/router/maintenance.ts`, `apps/server/src/routes/rpc/services/maintenance/**`, and `apps/server/src/routes/slack/interactions.ts`. (`page-urls.ts`, `service-adapter.ts`, `confirmation-store.ts`, and `workspace-resolver.ts` remain outside scope — they legitimately need db access for transport/URL/auth concerns.) - Subpath export `@openstatus/services/maintenance`. ## Tests - Integration tests in `packages/services/src/maintenance/__tests__/` cover happy paths, workspace isolation, cascade deletes, cross-workspace `NotFoundError` / `ForbiddenError`, the Zod date-range refine, the slack actor audit branch. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Mirrors the symmetric fixes applied to status-report on the parent branch: - **Dedupe `pageComponentIds`** in `validatePageComponentIds` — duplicate ids would violate the composite PK on `maintenance_to_page_component`. - **Batch list enrichment** — `listMaintenances` previously ran 2 extra queries per row; rewritten as one IN query regardless of list size, pairing well with the 10_000 sentinel tRPC passes. - **Idempotent tRPC `delete`** — swallow `NotFoundError` in the wrapper to preserve the old drizzle-returning behaviour; Connect still returns 404. - **Connect numeric-id error** — replace `invalidDateFormatError` with an inline `ConnectError(Code.InvalidArgument)` for malformed page component ids. Correct error message on the wire. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three issues mirror what landed on status-report after its PR-review pass — this branch was behind because it hadn't been rebased onto the updated status-report yet. **`update.ts` — pageId follows components** The service was enforcing a strict invariant: throw `ConflictError` when new components belonged to a different page, and leave `pageId` untouched when components were cleared. The Connect `UpdateMaintenance > clears pageId when removing all components` test fails against this — it expects `pageId` to null out on empty components. Same reasoning as the status-report fix: `pageId` should follow the association set. Mixed-page inputs still rejected upstream by `validatePageComponentIds`. **Handler test — error message wording** Two assertions expected the old handler's `"Start time (from) must be before end time"` message. Service throws `"End date must be after start date."` via `ConflictError`. Updated both assertions to match service wording — consistent with how the status-report message drift was resolved. **Service test — cleanup without try/finally** 7 tests did inline `db.delete(maintenance)` at the end of their body; a failing assertion skipped cleanup and orphaned rows. Replaced with a shared `createdMaintenanceIds` array drained in `afterEach`, matching the status-report pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
070ffab to
95d688f
Compare
Three CI failures on #2103: **Slack `toSlackMessage` — revert to uniform generic message.** The P3 fix on #2101 kept hand-written `ServiceError` messages for `FORBIDDEN` / `CONFLICT` / `VALIDATION` / `LIMIT_EXCEEDED` — but the existing `does not leak internal error details to user` test expects every error to surface as a single `"Something went wrong. Please try again."` string. The intent is stronger than just "strip row IDs": Slack users aren't developers, so none of the service error wording is appropriate for them. Reduced the adapter to a one-liner that always returns the generic message; detailed error context still flows to logtape/Sentry via the catch site upstream. **Handler test — "does not match the page ID" → "does not match".** `ConflictError` wording drifted during the migration (same as the status-report fix). Loosened to the stable fragment. **Handler test — "Page not found" → "not found".** `NotFoundError("page", id)` formats as `"page <id> not found"` (lowercase `page`). Pre-migration handler emitted capital-P `"Page not found"`. Loosened the substring so both forms pass. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Valid Cubic finding on the previous commit: the `VALID_THEMES` local const duplicated the `THEME_KEYS` enum from `@openstatus/theme-store` and would drift, silently downgrading valid themes to `"default"` on update if a new theme got added to the canonical list but not here. Replaced the hardcoded array + `ValidTheme` local type with a direct import of `THEME_KEYS` / `ThemeKey` from `@openstatus/theme-store`. `@openstatus/server`'s package.json already declares the workspace dep, so no deps change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment |
The `createNotification > throws ForbiddenError for cross-workspace monitor` test uses `freeCtx` and expects the cross-workspace monitor check in `validateMonitorIds` to fire. But `createNotification`'s first gate is `count() >= notification-channels` (free plan = 1), so a single leftover row on workspace 2 from a previous test or aborted run trips `LimitExceededError` before the monitor check runs, and the assertion sees the wrong error type. Cleared the free workspace's notifications at the top of `beforeAll` so the suite starts clean regardless of prior state. Leaving `Status Route: Incident detection > returns incident status with ongoing incident` alone — still failing with `Received: "operational"` from a preceding in-file code path; a pre-existing issue not caused by my recent commits, worth its own investigation. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…on casts
The hardcoded `as "default" | "default-rounded" | …` and
`as "duration" | "requests" | "manual"` string unions in the form
update-mutation callsites duplicated `THEME_KEYS` / the enum members
of `pageConfigurationSchema` — Cubic flagged this as drift risk
("downgrades valid themes to default on update" if the canonical
enum gains a member without a matching copy update).
Replaced the hardcoded unions with derived type references:
- `apps/dashboard/src/components/forms/components/update.tsx`:
imports `PageConfiguration` from `@openstatus/db/src/schema` and
casts each field to `PageConfiguration["theme"|"value"|"type"]`.
- `apps/dashboard/src/components/forms/status-page/update.tsx`:
imports `ThemeKey` from `@openstatus/theme-store` and casts the
`theme` field to it.
- `packages/db/src/schema/pages/validation.ts`: exports a new
`PageConfiguration = z.infer<typeof pageConfigurationSchema>`
type so dashboard callers can cast to the canonical shape
without reaching into service internals (dashboard has
`@openstatus/db` but not `@openstatus/services`).
The form schemas stay loose — that keeps the RHF resolver contract
intact and keeps the call-site refactor minimal. Invalid submits
still get caught by the tRPC router's zod parse at the boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…cases
Connect `createMaintenance` was echoing `req.pageComponentIds` (raw
proto strings) back in the response instead of the stored set, so a
request with duplicate ids or strings that got parsed/deduplicated
downstream returned a payload that didn't match persisted state.
`updateMaintenance` already re-fetches after insert to return
canonical ids; `createMaintenance` now does the same — matching
`createNotification` too.
Also:
- Drop dead `MaintenanceDateRange` re-export from the service
schemas (no consumers; the "refine is unused" comment no longer
applies since the refine lives on `CreateMaintenanceInput` directly).
- Document the `listMaintenances` `totalSize` + page race — best-effort
under concurrent inserts, kept explicit so PR 3/4 don't copy it as
load-bearing.
- Add four regression tests:
- `createMaintenance` deduplicates repeated `pageComponentIds`
(guard for the `Set` dedupe → composite-PK violation).
- `createMaintenance` throws `ConflictError` on components that
span multiple pages.
- `updateMaintenance` throws `ConflictError` on a partial update
that moves `to` before the stored `from` (the effective-range
branch only reachable via update, previously untested).
- `updateMaintenance` throws `ConflictError` on mixed-page
`pageComponentIds`.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Most of the recent "mystery" CI failures have been the same shape: a negative-path test uses `freeCtx` and expects a specific error (ForbiddenError, LimitExceededError on the plan-gate, etc.), but the `free` plan's tight caps (`status-pages: 1`, `notification-channels: 1`) have already been consumed by leftover rows on workspace 2 — usually from an aborted prior run. The service throws a quota-hit error *before* the assertion target fires, and the test reports a wrong error type. Added a reusable `cleanQuotaGatedTables(workspaceId)` helper in `packages/services/test/helpers.ts` that wipes the two tables that have recurrently triggered this (`page` + dependent `pageComponent`, `notification`). Call it in `beforeAll` of any suite that exercises `freeCtx` on a write path. Applied to the two suites that have been tripping — fixes this round's failures: - `createPage (full form) > rejects cross-workspace monitor` — hit `status-pages limit reached (1)` before the monitor workspace check fired. - `updatePageLocales > rejects when plan lacks i18n` — hit the same status-pages cap while `newPage`-ing for the test setup. - (Already covered earlier:) `createNotification > throws ForbiddenError for cross-workspace monitor` — replaced the inline `delete notification` cleanup with the shared helper. Extend the helper's delete list when a new quota-gated table starts causing the same pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
@claude review |
|
Claude finished @mxkaske's task in 5m 23s —— View job ReviewGood news: all five issues from the Apr 23 review are resolved in the current state. A few new things worth a look, in descending priority. Dead code in
|
|
@cubic-dev-ai review |
@mxkaske I have started the AI code review. It will take a few minutes to complete. |
Three items from the latest Claude pass on #2103, all low-risk: **Dead code in `maintenance/errors.ts` — four unused helpers.** `pageComponentNotFoundError`, `pageComponentsMixedPagesError`, `pageNotFoundError`, and `pageIdComponentMismatchError` were exported but never imported anywhere — all their call-paths route through `toConnectError` in the shared adapter, which already emits usable generic codes. Removed the four helpers plus their corresponding `ErrorReason` enum entries. If clients later need the richer structured-error headers those helpers carried, reintroduce them alongside the per-catch wiring then. **`parsePageComponentIds` coerced `""` to 0.** `Number("")` is `0` (finite), so an empty-string id in the proto-repeated field slipped through the guard and became component-id 0. The service's `NotFoundError` surfaced that as a misleading 404 on the wire instead of the correct `InvalidArgument` at the handler. Switched to `Number.parseInt(id, 10)`, which returns NaN for `""` and keeps the existing finite-check correct for every other case. **Atomicity note on the `notify=true` path.** Claude flagged that `createMaintenance` + `notifyMaintenance` aren't wrapped in a single atomic unit: the row is committed first, so a throw in the dispatch surfaces as 500 and a retry can create a duplicate. This is a conscious trade — wrapping the dispatch in the write tx would hold a row lock across an external notification fan-out, which is worse. `status-report` makes the same choice. Added a block comment documenting the rationale so the next reader doesn't "fix" it into a transaction. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Stacked on #2101. Second domain onto the service layer. Every maintenance write and read goes through
@openstatus/services/maintenance; tRPC / Connect / Slack are thin adapters. Same shape as the status-report PR.What lands
Services (
packages/services/src/maintenance/)createMaintenance,updateMaintenance,deleteMaintenance,listMaintenances,getMaintenance,notifyMaintenance.from < to) enforced via Zod refine onCreateMaintenanceInputand again insideupdateMaintenancewhen the partial update could cross the invariant.withTransaction, emitemitAuditbefore returning.validatePageComponentIds,updatePageComponentAssociations,getMaintenanceInWorkspace,getPageComponentIdsForMaintenance,getPageComponentsForMaintenance) duplicated from status-report for this PR. Noted as the extraction trigger once a third consumer arrives.Surfaces — external contracts preserved
packages/api/src/router/maintenance.ts):delete/list/new/updatecall services.newstill returns{ ...maintenance, notifySubscribers }for dashboard consumers.listpasseslimit: 10_000(same sentinel strategy as the status-report list after the PR 1 Cubic fix).apps/server/src/routes/rpc/services/maintenance/): proto → parse → service → convert; external Connect API unchanged.interactions.ts):createMaintenancebranch goes through services. Also extractsgetPageUrl/getReportUrlintoapps/server/src/routes/slack/page-urls.tssointeractions.tsbecomes db-free and can be added to the Biome scope.packages/api/src/router/email/index.ts):sendMaintenancebecomes a thin wrapper overnotifyMaintenance.Enforcement
Biome
noRestrictedImportsscope now also covers:packages/api/src/router/maintenance.tsapps/server/src/routes/rpc/services/maintenance/**apps/server/src/routes/slack/interactions.tsCarve-outs (outside scope):
slack/page-urls.ts,slack/service-adapter.ts,slack/confirmation-store.ts,slack/workspace-resolver.ts— all legitimately need db for transport/URL/auth concerns.Subpath export
@openstatus/services/maintenance.Tests
packages/services/src/maintenance/__tests__/maintenance.test.ts— happy paths, workspace isolation, cascade deletes, cross-workspaceNotFoundError/ForbiddenError, Zod date-range rejection, slack-actor audit branch.Behaviour-preserving notes
deleteMaintenancestill returns{ success: true }; tRPC'sdeletereturned.returning()rows which dashboard ignored. Returning[] as Array<never>preserves the observable shape (an empty array is indistinguishable from "no rows deleted" in the old code path when the mutation succeeds).updateMaintenanceConnect semantics preserved:updatePageComponentIds: falseleaves associations alone;trueclears-and-replaces with the providedpageComponentIds.Test plan
maintenance.test.tsHTTP suite still green (external contract unchanged)/maintenancespage — create / update / delete round-trips, notify flag still triggers the two-call patterncreateMaintenanceaction (approve/approve_notify/cancel) round-tripsStack
🤖 Generated with Claude Code