Skip to content

feat(frontend): typed API client generated from the OpenAPI spec + gen-verify CI#59

Open
AgentWrapper wants to merge 2 commits into
mainfrom
feat/projects-openapi-codegen
Open

feat(frontend): typed API client generated from the OpenAPI spec + gen-verify CI#59
AgentWrapper wants to merge 2 commits into
mainfrom
feat/projects-openapi-codegen

Conversation

@AgentWrapper

@AgentWrapper AgentWrapper commented May 31, 2026

Copy link
Copy Markdown
Contributor

Why (scope change)

This PR originally added the code-first OpenAPI generator + a frontend client. The backend generator has since landed on main (via #68/#65) and now covers projects and sessions — superseding this branch's backend work. Rather than duplicate it, this PR is repurposed to the parts main still lacks.

What's here

  • frontend/src/api/schema.d.ts — generated from backend/internal/httpd/apispec/openapi.yaml via openapi-typescript (npm run gen:api). Covers projects, sessions, and orchestrators.
  • frontend/src/api/client.ts — a small openapi-fetch client typed by that schema, so the renderer's request/response types come from the daemon contract instead of being hand-maintained.
  • gen-verify CI job — regenerates the spec from Go and the TS client from the spec, failing if either committed artifact is stale. Backend drift is already covered by the apispec tests; this additionally guards the frontend artifact, which nothing else checks.

Verification

frontend: npm run gen:api ✓   npm run typecheck ✓
pipeline: go generate ./... + npm run gen:api reproduce the committed files with no drift

Rebased on current main; no overlap with the backend codegen or the recent CLI PRs.

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a typed openapi-fetch client for the Electron frontend, backed by the already-landed schema.ts generated from the backend OpenAPI spec. Port resolution is correctly deferred to the wiring layer rather than hardcoded.

  • frontend/src/api/client.ts — exports createApiClient(baseUrl), baseURLForPort(port), and DEFAULT_PORT (documented as a last-resort fallback), plus re-exports paths and components types from schema.ts for renderer consumption.
  • frontend/package.json / package-lock.json — adds openapi-fetch@0.17.0 and its openapi-typescript-helpers@0.1.0 transitive dep as runtime dependencies, correct for a tsc-only (non-bundler) Electron project.

Confidence Score: 5/5

Safe to merge — the change is a thin, well-documented client wrapper with no runtime logic beyond URL construction and type re-exports.

The client module introduces no application logic that could regress: it delegates URL construction to the caller, exports a fallback constant with explicit comments warning against relying on it, and passes types straight through from the generated schema. The dependency additions are minimal and the lock file hashes are well-formed.

No files require special attention.

Important Files Changed

Filename Overview
frontend/src/api/client.ts New typed API client wrapping openapi-fetch; correctly defers port/URL resolution to the caller, exports DEFAULT_PORT only as a documented fallback, and re-exports generated types for the renderer.
frontend/package.json Adds openapi-fetch as a runtime dependency (correct placement for an Electron app built with tsc, not a bundler).
frontend/package-lock.json Lock file updated with openapi-fetch@0.17.0 and its transitive dependency openapi-typescript-helpers@0.1.0; SHA-512 integrity hashes are well-formed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Renderer Process] -->|calls| B[createApiClient baseUrl]
    C[Main Process / IPC] -->|reads running.json Port| D[baseURLForPort port]
    D -->|http://127.0.0.1:port| B
    E[DEFAULT_PORT = 3001] -.->|last-resort fallback only| D
    B -->|createClient paths baseUrl| F[openapi-fetch Client]
    F -->|typed GET/POST/PATCH/DELETE| G[Daemon /api/v1/...]
    H[schema.ts generated] -->|paths, components types| B
Loading

Reviews (2): Last reviewed commit: "fix(frontend): derive API base URL from ..." | Re-trigger Greptile

Comment thread backend/internal/httpd/controllers/dto.go
Comment thread backend/internal/httpd/apispec/specgen/build.go
Comment thread backend/internal/httpd/apispec/specgen/build.go Outdated
Comment thread backend/internal/httpd/controllers/dto.go
AgentWrapper pushed a commit that referenced this pull request May 31, 2026
…chema names

Resolve the four review comments on #59:

- ProjectOrDegraded.MarshalJSON now errors when neither Project nor Degraded
  is set instead of silently emitting `{"project": null}`, which would breach
  the required oneOf[Project, Degraded] contract.
- requestBody.required: true is now set for addProject and updateProjectConfig
  via WithCustomize — swaggest left it absent (== optional) before.
- schemaName replaces the TrimPrefix catch-all with an exhaustive default→clean
  map; an unrecognised type is returned verbatim so it surfaces in the diff
  rather than silently colliding with an existing schema.
- nonNullableSlices strips the spurious "null" swaggest unions into every Go
  slice, so `projects` is `Summary[]` not `Summary[] | null`; the list handler
  normalises a nil slice to [] so the wire matches the non-nullable schema.

Regenerated openapi.yaml + frontend schema.d.ts. Refs #59.
@AgentWrapper AgentWrapper force-pushed the feat/projects-openapi-codegen branch from 0de7ce0 to 49c7883 Compare May 31, 2026 22:12
Comment thread backend/internal/httpd/controllers/dto.go
AgentWrapper pushed a commit that referenced this pull request May 31, 2026
Addresses the P1 follow-up on #59: returning an error from
ProjectOrDegraded.MarshalJSON does not "surface" the contract violation —
envelope.WriteJSON discards the encode error after the 200 status and a partial
JSON frame have already been flushed, leaving the client with a truncated,
unparseable 200 (worse than the previous null).

Validate the invariant upstream instead: newGetProjectResponse now returns an
error when the GetResult sets neither Project nor Degraded, and the get handler
maps that to a 500 envelope before any status/body is written. MarshalJSON keeps
the error branch only as an unreachable last-resort backstop, with the comment
corrected to say so. Adds TestProjectsAPI_GetEmptyResultIs500 to lock the
clean-500 behavior.

Refs #59.

@neversettle17-101 neversettle17-101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes LGTM

neversettle17-101 added a commit that referenced this pull request Jun 1, 2026
…penAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
…chema names

Resolve the four review comments on #59:

- ProjectOrDegraded.MarshalJSON now errors when neither Project nor Degraded
  is set instead of silently emitting `{"project": null}`, which would breach
  the required oneOf[Project, Degraded] contract.
- requestBody.required: true is now set for addProject and updateProjectConfig
  via WithCustomize — swaggest left it absent (== optional) before.
- schemaName replaces the TrimPrefix catch-all with an exhaustive default→clean
  map; an unrecognised type is returned verbatim so it surfaces in the diff
  rather than silently colliding with an existing schema.
- nonNullableSlices strips the spurious "null" swaggest unions into every Go
  slice, so `projects` is `Summary[]` not `Summary[] | null`; the list handler
  normalises a nil slice to [] so the wire matches the non-nullable schema.

Regenerated openapi.yaml + frontend schema.d.ts. Refs #59.
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
Addresses the P1 follow-up on #59: returning an error from
ProjectOrDegraded.MarshalJSON does not "surface" the contract violation —
envelope.WriteJSON discards the encode error after the 200 status and a partial
JSON frame have already been flushed, leaving the client with a truncated,
unparseable 200 (worse than the previous null).

Validate the invariant upstream instead: newGetProjectResponse now returns an
error when the GetResult sets neither Project nor Degraded, and the get handler
maps that to a 500 envelope before any status/body is written. MarshalJSON keeps
the error branch only as an unreachable last-resort backstop, with the comment
corrected to say so. Adds TestProjectsAPI_GetEmptyResultIs500 to lock the
clean-500 behavior.

Refs #59.
@AgentWrapper AgentWrapper force-pushed the feat/projects-openapi-codegen branch from 63f5daa to 4645ab9 Compare June 1, 2026 16:27
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
…penAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
AgentWrapper pushed a commit that referenced this pull request Jun 1, 2026
* refactor(project): manager talks to the sqlite store; drop the in-memory store

The project Manager now runs only against the durable backend store: remove the
process-local MemoryStore (and NewMemoryManager), and require a real Store. The
daemon already wires the sqlite store; tests now build a real temp-dir sqlite
store instead of the mock.

- Move Row + the Store port to project/store.go. The Store interface stays
  because it is the dependency-inversion port that lets the manager reach the
  backend without an import cycle (storage imports project.Row), not an extra
  mock layer — there is no longer any in-memory implementation.
- NewManager requires a non-nil Store (no in-memory fallback).
- Add project/manager_test.go: List/Add/Get/Remove happy paths +
  PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED,
  PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real
  sqlite store (the service-logic tests #47 lacked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(project): trim routes, consolidate package, add code-first OpenAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(project): address PR review comments

- t.Skipf → t.Fatalf in gitRepo helper: git failures now hard-fail
  instead of silently skipping manager tests on a misconfigured runner
- FindProjectByPath: add AND archived_at IS NULL so archived paths don't
  permanently block re-registration (update queries/projects.sql and
  generated gen/projects.sql.go)
- Add TestManager_ReaddAfterRemove to lock the fix

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fixed lint and fmt

* addressed greptile comments

* Apply suggestions from code review

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* project tests fix

* project_tests fix

* fix: Linting and formatting fix

* refactor: move project manager into service layer (#68)

* refactor: split service package by resource (#68)

* fix: ignore archived project id conflicts (#68)

* refactor: move pr manager into service layer (#68)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
@AgentWrapper AgentWrapper force-pushed the feat/projects-openapi-codegen branch 2 times, most recently from 064c188 to 019c985 Compare June 2, 2026 16:18
@AgentWrapper AgentWrapper changed the title feat(api): code-first OpenAPI generation + typed frontend client feat(frontend): typed API client generated from the OpenAPI spec + gen-verify CI Jun 2, 2026
Main already generates frontend/src/api/schema.ts from the backend OpenAPI
document (#103) but ships only the types. This adds the thin runtime layer:
an openapi-fetch client typed by those generated paths, so the renderer makes
fully typed requests against the daemon's loopback /api/v1 surface instead of
hand-writing fetch calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AgentWrapper AgentWrapper force-pushed the feat/projects-openapi-codegen branch from 019c985 to c5d667f Compare June 8, 2026 16:09
Comment thread frontend/src/api/client.ts Outdated
The daemon binds AO_PORT (default 3001) and writes the actual port to
running.json. Baking http://127.0.0.1:3001 into the client meant any user
overriding AO_PORT would silently hit the wrong address. Replace the
pre-built constant-port client with a createApiClient(baseUrl) factory plus
a baseURLForPort helper, so the wiring layer passes the port discovered from
running.json (via IPC). DEFAULT_PORT remains only as a documented fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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