Skip to content

refactor(console): migrate view layer to OpenAPI client (replaces #187)#241

Closed
jeroenrinzema wants to merge 28 commits into
mainfrom
feat/console-openapi-migration
Closed

refactor(console): migrate view layer to OpenAPI client (replaces #187)#241
jeroenrinzema wants to merge 28 commits into
mainfrom
feat/console-openapi-migration

Conversation

@jeroenrinzema

@jeroenrinzema jeroenrinzema commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Replacement for #187 (closed). Migrates the Console view layer from the legacy axios-based api client to the typed openapi-fetch client (oapiClient), against the current management OpenAPI spec.

Unlike #187 (which was ~3 months stale, built against an old spec with wrong paths, and accumulated ~25 unaddressed review items), this PR is built fresh on main, and every call is verified by tscopenapi-fetch is fully typed, so an incorrect path/param/body fails to compile.

What changed

  • Added src/lib/path-suggestions.tsfetchPathSuggestions(), a typed replacement for the api.projects.pathSuggestions aggregator.
  • Migrated ~45 view/component files across users, campaign, journey, project, settings, broadcast and shared components to oapiClient.
  • Tightened a few spec-gap casts (journey status/tags, entrance_id, cursor pagination) to scoped, documented casts rather than blanket as never.

Intentionally left on the legacy client

These have no endpoint in the management spec (the same gaps that blocked #187) or no clean openapi-fetch equivalent:

  • Journey: entrances (search/log), steps/{stepID}/users, user skip-delay / remove-from-journey
  • List user upload (POST /lists/{listID}/users) and multipart imports (users/documents)
  • Broadcast SSE progress stream (apiUrl + EventSource)
  • Auth flows (Login, LoginCallback)
  • The generic ProjectEntityPath route abstraction (router.tsx, createStatefulRoute.tsx) and the mod.ts SDK re-export

api.ts therefore remains (it is not dead) — this continues main's incremental migration.

Known spec inaccuracy (follow-up)

getCampaign is declared in resources.yml as { data: Campaign }, but the Go handler (campaigns.go:204) returns the Campaign directly via json.Write(..., campaign.OAPI()). The client reads it per the backend reality (documented inline). The spec should be corrected (and Go/TS types regenerated) in a follow-up — out of scope here to keep this PR focused and avoid regenerating backend bindings.

Verification

  • tsc --noEmit: 0 errors
  • pnpm build: succeeds
  • eslint on changed files: clean (remaining repo warnings are pre-existing, in untouched files)
  • No console test suite exists (vitest finds no test files)
  • Audited all migrated single-resource GETs against their Go handlers to confirm wrapper handling; verified no {data}-wrapper / direct-response mismatches remain beyond the documented getCampaign case.

jeroenrinzema and others added 27 commits June 16, 2026 22:23
Add store-level tests asserting ListUserSchedules / ListOrganizationSchedules
report has_pending_events=true while events are unfired and flip to false once
all events are marked fired. Guards the badge-stays-lit-forever regression that
this branch fixes.
- Regenerate console management OpenAPI bindings after the inbox
  open->read rename (read_at, /read paths)
- Update inbox-detail-table console consumer to the read terminology so
  it typechecks against the regenerated types; fixes prettier formatting
- Restore oapi.ListStateDraft/ListStateReady constants in lists_test.go
- Use *int16 (ptr.To) for InboxMessage.Priority in consumer test
- Fix client inbox spec tests: /inbox/read path and required identifier
- SMS template data: align textTemplateDataSchema and TextTemplateData to
  the API shape ({ body }); fix SMS preview reading data.text -> data.body
- Enable mode: "onChange" on NewCampaign and ProjectOnboardingDomain forms
  so formState.isValid updates and the submit button isn't stuck disabled
- optionalPhoneSchema: accept "" so an InlineEdit phone can be cleared
- Trim names/IDs in list, journey, and device form schemas to reject
  whitespace-only input
- Users.createUser: optional-chain optional fields to avoid a TypeError
  when only some identity fields are provided
- EmailContentPreview: drop render-phase setCompiledHtml("") (effect already
  clears it) and return null instead of undefined for non-email templates
- searchTimeoutRef: initialize empty instead of scheduling a throwaway timer
Replace the hand-rolled SHA-256/HMAC implementation with crypto/sha256 +
crypto/hmac. The "stdlib panics under WASM" rationale does not hold for the
current TinyGo wasi toolchain (verified: stdlib sha256/hmac produce correct
known-answer values under tinygo 0.40.1 + wasmtime), and the Mailgun provider
already relies on stdlib crypto. This removes ~130 lines of unaudited custom
crypto from the SES request signer.

Also set Spec.Webhook to false: WebhookHandler is a no-op stub that drops all
events (SNS parsing is unimplemented), so advertising webhook support would
register an endpoint that silently discards every delivery/bounce event.
The unused linter now flags it since the consumer test package compiles
again (it was masked by the prior inbox_test.go compile error).
Amazon SES and Mailgun declared Platforms: [PlatformEmail], but Spec.Platforms
is the push-platform list. On provider creation, autoAssignPushProvider
upserts the provider as the default push provider for every platform it lists,
bypassing the validPlatforms allowlist (ios/android/web only). This wrote an
invalid "email" row into project_push_providers for what are email-channel
providers.

Email providers are identified by Channels: [ChannelEmail] (as resend already
does); they have no push platform. Drop the Platforms declaration from both and
remove the now-unused PlatformEmail constant, reverting channel.go to its
prior state.
Route due inbox messages through the inbox process pipeline (with a stable
Msg-Id for idempotent re-injection) instead of emitting an analytics event,
and gate dispatch on IsDue() so future-scheduled messages published
immediately by the client/campaign paths wait for the scheduler. Also guard
generic email/SMS composition against an empty recipient.
Move the destructive 'DROP TABLE campaign_sends' out of the inbox-table
creation migration into its own migration with a down migration that recreates
the table structure (its final post-broadcast_id, composite-PK shape). The
inbox migration's down is now a clean inverse. Also correct the ScanDue*
comments: FOR UPDATE SKIP LOCKED is not run in a held transaction, so
idempotency actually comes from the scheduler's stable Msg-Id and the sent_at
guard, not the row lock.
Backend
- Fix UnarchiveList: drop the GetList pre-check that filtered deleted_at IS
  NULL, which made every restore return 404; rely on the store returning
  sql.ErrNoRows on zero rows affected.
- Fix UnarchiveCampaign handler: add the missing generic error branch so
  non-ErrNoRows DB errors no longer fall through to a false 204.
- Make UnarchiveJourney store return sql.ErrNoRows on zero rows affected,
  matching the list/campaign stores, and simplify the handler.
- Change the list filter semantics from "include deleted (both)" to
  "archived only" so the archived view can paginate server-side with a
  correct total_count. Renamed the store/controller param to archivedOnly
  and updated the include_deleted OAPI parameter description accordingly.

Frontend
- Replace the client-side limit:100 + .filter() archived view with real
  server-side offset pagination in the Lists, Campaigns and Journeys views
  (dedicated archivedOffset, prev/next, footer shown in both views, offset
  reset on toggle/search/unarchive).

Tests
- Add store tests covering the archived-only filter and the unarchive happy
  path plus ErrNoRows on already-active and non-existent rows, for lists,
  campaigns and journeys.
fix: add has_pending_events field to user and organization schedules
…-components-and-provider-crud

fix: enhance components with error handling and improve rate limit logic
…lidation

# Conflicts:
#	console/src/views/settings/IntegrationSetup.tsx
#	console/src/views/users/ListCreateForm.tsx
Normalize arbitrary timezone formats (GMT+2, EST, UTC+05:30, etc) to canonical IANA strings during CSV import. Includes timezone resolver, tests, and timezone selection UI improvements.
…lidation

# Conflicts:
#	console/src/views/users/Users.tsx
# Conflicts:
#	internal/http/controllers/v1/client/oapi/resources_gen.go
#	internal/http/controllers/v1/management/oapi/resources_gen.go
#	internal/wasm/test/provider.wasm
Add inbox messaging system for organizations and users
…s-provider

# Conflicts:
#	go.mod
#	go.sum
#	internal/http/controllers/v1/client/oapi/resources_gen.go
#	internal/http/controllers/v1/management/oapi/resources_gen.go
#	internal/wasm/test/action.wasm
#	internal/wasm/test/provider.wasm
…-files

# Conflicts:
#	go.mod
#	go.sum
#	internal/http/controllers/v1/client/oapi/resources_gen.go
#	internal/http/controllers/v1/management/oapi/resources_gen.go
#	internal/store/journey/journeys_test.go
feat: enhance journey, campaign, and list management with unarchive f…
… errors (#236)

Replaces window.alert/window.confirm usage with a shadcn AlertDialog for project save errors. Closes #168.
Implements the SendGrid email provider WASM module: API-based email sending, webhook event handling mapped to canonical events, configuration schema (API key + webhook verification key), and tests. Closes #216.
…s/subscriptions to oapiClient

Establishes the openapi-fetch migration pattern: replace the legacy axios
'api' object with the typed oapiClient. Adds src/lib/path-suggestions.ts to
replace the api.projects.pathSuggestions aggregator.
…lient

Replaces the legacy axios-based `api` object with the typed openapi-fetch
`oapiClient` across the users, campaign, journey, project, settings and
broadcast views. Each call was verified against the generated OpenAPI types
(tsc is a hard gate, since openapi-fetch is fully typed).

Calls with no management-spec endpoint are intentionally left on the legacy
client: journey entrances / step-users / skip-delay / remove-from-journey,
list user upload, multipart imports (users/documents), the broadcast SSE
progress stream, the auth flows, and the generic ProjectEntityPath route
abstraction.

Note: getCampaign is declared in the spec as { data: Campaign } but the
backend returns the Campaign directly; the client reads it per the backend
(documented inline) pending a spec correction.
@jeroenrinzema jeroenrinzema force-pushed the feat/console-openapi-migration branch from c4a44f7 to 6f5c784 Compare June 17, 2026 12:24
The getCampaign OpenAPI response was declared as { data: Campaign }, but the
handler (campaigns.go GetCampaign) returns the Campaign directly via
json.Write(w, ..., campaign.OAPI()) — matching every other single-resource GET
(getJourney/getProject/getUser/getList). The wrapper was an outlier that made
the generated clients describe a response shape the backend never sends.

Corrects the spec and regenerates the Go (resources_gen.go) and TypeScript
(management.generated.ts) bindings. No Go code consumed the old wrapped shape.
@jeroenrinzema

Copy link
Copy Markdown
Contributor Author

Closing as superseded.

Most of this PR's diff already landed on main via other PRs. Re-basing it three-ways (base vs origin/main vs this branch, merge-base 1683f7e0) shows its real net delta would now revert main's Access/clients feature and resurrect deleted files (e.g. ApiKeys.tsx). So merging it as-is would be a regression.

The genuinely salvageable part — the view-layer migration from the legacy API client to the generated OpenAPI client (oapiClient) on files that main has not touched since this PR forked — has been cleanly relanded in #269, verified tsc-clean against main's baseline (zero new type errors).

Still needs a manual re-migration on top of main (NOT done in #269)

These 22 files were changed by both main and this PR in parallel, so they genuinely conflict and could not be auto-relanded. Their oapiClient migration needs to be re-applied by hand on top of current main so the work isn't lost:

  • console/src/components/scheduled-detail-table.tsx
  • console/src/oapi/management.generated.ts
  • console/src/types.ts
  • console/src/validation/campaign/new-campaign.ts
  • console/src/validation/campaign/template/data.ts
  • console/src/views/broadcast/BroadcastMessagePreview.tsx
  • console/src/views/broadcast/CreateBroadcastDialog.tsx
  • console/src/views/campaign/CampaignDetails.tsx
  • console/src/views/campaign/Campaigns.tsx
  • console/src/views/campaign/NewCampaign.tsx
  • console/src/views/journey/JourneyForm.tsx
  • console/src/views/journey/Journeys.tsx
  • console/src/views/journey/steps/Entrance.tsx
  • console/src/views/project/ProjectForm.tsx
  • console/src/views/settings/ApiKeyDialog.tsx
  • console/src/views/settings/Subscriptions.tsx
  • console/src/views/users/ListCreateForm.tsx
  • console/src/views/users/Lists.tsx
  • console/src/views/users/UserDetailAttrs.tsx
  • console/src/views/users/UserDetailEvents.tsx
  • console/src/views/users/UserDetailIdentifiers.tsx
  • console/src/views/users/Users.tsx

Additionally, 12 view files that only this PR migrated could not be relanded cleanly because they depend on this PR's migrated types.ts/generated symbols (they error against main's legacy types); they are listed in #269 and also need re-migration on top of main.

jeroenrinzema added a commit that referenced this pull request Jun 24, 2026
refactor(console): reland oapiClient view-layer migration (supersedes #241)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants