Skip to content

docs(api): document search and links routes - #1763

Open
zorak1103 wants to merge 6 commits into
perber:mainfrom
zorak1103:docs/api-search-links
Open

zorak1103 wants to merge 6 commits into
perber:mainfrom
zorak1103:docs/api-search-links

Conversation

@zorak1103

@zorak1103 zorak1103 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Related issue

Part of #1661. This is PR 4, "search, links". It follows the pattern from #1682 and #1741.

What changed

This documents the 4 remaining read-only routes of internal/wiki/search and internal/wiki/links: GET /api/search, GET /api/search/status, GET /api/pages/{id}/links and GET /api/links/broken. It removes them from the allowlist in internal/http/api_docs_allowlist_test.go. Routes, status codes and JSON keys are unchanged. The only wire changes are the two null to [] fixes below, each in its own commit with a Behavior-Change: trailer.

The PR has 6 commits:

  1. test(wiki): route tests for both packages, run through the real router, pinning today's wire format (shape, query semantics, exact error bytes, 401 and 200). GET /api/links/broken is tested with a real auth service with an admin and an editor, because RequireAdmin always answers 403 when authentication is disabled and would hide the 200 case. All of these tests pass against the old code.
  2. fix(search): GET /api/search sends "items": [] instead of "items": null when a text query matches nothing. The early returns and the tag-only path already sent []. The UI already tolerates both (data.items || []). Behavior-Change: trailer.
  3. refactor(wiki): the package-local error helpers in search/errors.go and links/errors.go move to dto.ErrorResponse/dto.ErrorDetail, as docs/api/README.md asks. The bytes are identical and the helper names stay.
  4. refactor(wiki): the four handlers serialize dto types through new mappers (dto.ToSearchResult, ToIndexingStatus, ToLinkStatus, ToBrokenLinks) instead of core types. This is byte-identical, which the commit-1 tests and marshal-parity tests in dto show.
  5. fix(links): GET /api/pages/{id}/links sends [] instead of null for backlinks and broken_incoming of a page without links (outgoings and broken_outgoings already did). The generated spec marks all four lists as required, and the README says to normalize nil slices in required fields. The UI already uses status?.backlinks ?? []. Behavior-Change: trailer.
  6. docs(api): swag annotations, regenerated docs/api, the four allowlist entries removed, README and ADR 0014 updated.
    • Errors: 400 and 404 are ErrorResponse with the codes named. 401, 403, 429 and 503 are LegacyError. 500 is a broad object where the handler can fail, because the handler and the auth middleware return different shapes (same workaround as tags). /search/status documents no 400 and a LegacyError 500 only, because its handler cannot fail.
    • Access: /links/broken is documented as admin only, also with public access on. API keys act with the lesser of owner and key role, and with authentication disabled the route answers 403.
    • Spec test: assertOperation accepts an object 200 response (okRef/okProps) and path parameters, and a nil parameter map now means "none". The existing tags and properties tests are unchanged. The new TestAPIDocs_Search_DescribeReadOperations and TestAPIDocs_Links_DescribeOperations also pin the nested models, and the ten new models are in documentedModels.

Inconsistencies found (for the list in #1661, not fixed here)

  • limit/offset differ by path. Text search hands them to SQLite: limit=-1 returns all rows, limit=0 returns no items, and there is no upper bound. Tag-only search turns limit <= 0 into 20 and clamps offset. Tags and properties clamp at 200.
  • The 400 for a missing query says "q is required", although tags alone is enough.
  • Search and links use snake_case keys, while tags and pages use camelCase.
  • Broken links wrap the list in {"links": ...}, while other lists are bare arrays.
  • finished_at on /api/search/status is 0001-01-01T00:00:00Z until the first index run finishes.
  • The localized search_unavailable and link_service_unavailable paths are unreachable in a real Wiki: initSearch and initLinkService abort construction on error, so neither dependency can be nil. They are left in place.

How I tested it

  • go build ./... and go test pass for ./internal, ./internal/http/..., ./internal/wiki/search, ./internal/wiki/links, ./internal/search and ./internal/links.
  • make api-docs followed by git diff --exit-code -- docs/api shows no drift. make verify-commits BASE=origin/main passes.
  • golangci-lint finds nothing in the touched packages. GOOS=linux and GOOS=darwin go vet pass for the touched packages.
  • I tested on Windows only. Linux runs rely on this PR's CI.
  • The test-go-windows job failed in internal/restore, a package this PR does not touch. It is a race in the test, fixed in test(restore): wait for the upload cleanup before checking the temp file #1764. Once that is merged, this PR needs a rebase or a re-run of the job.
  • Not run locally: make verify as a whole (its recipe fails on a Windows path quirk; I ran scripts/check-private-references.sh directly) and the E2E suite.
  • Two paths are not covered by a route test: broken_incoming with a non-empty list (it needs a deleted or moved target page; it shares its item type with backlinks, whose keys are pinned), and a nil status in ToIndexingStatus (unreachable in a real Wiki, pinned in the dto test).

Checklist

  • I discussed the approach on the linked issue before starting (or this is a trivial fix: typo, docs, obvious one-liner)
  • This PR is focused on a single change
  • New behaviour plugs into an extension point; no new if feature branches in shared code (rule 1)
  • I updated documentation if needed
  • If users or admins will notice a change in behaviour, the commit carries a Behavior-Change: trailer (commit style)
  • For UI changes: behaviour is covered by automated tests where possible (rule 11) (no UI changes)
  • For UI changes: I checked by hand what tests can't cover (look in light/dark mode, mobile width where relevant) (no UI changes)
  • AI tools were used for this PR (name them: Claude Code, Claude Sonnet 5.5). I have reviewed and understand every line

Pin the current wire format of GET /api/search, /api/search/status,
/api/pages/{id}/links and /api/links/broken through the real router, so
the following refactors can be proven byte-identical.

The links fixture uses a real auth service with an admin and an editor,
because RequireAdmin always answers 403 with authentication disabled and
would otherwise hide the 200 case.
SQLiteIndex.Search left Items nil for a text query without a match, so
GET /api/search serialized "items": null. The early returns and the
tag-only path already send an empty list; make the text path consistent.

Behavior-Change: GET /api/search now returns "items": [] instead of "items": null when a text query matches nothing.
Both slices carried their own copy of the localized error envelope,
byte-identical to dto.ErrorResponse. Write the shared type and delete
the copies, as the tags slice already does. The helper names and the
wire format stay unchanged.
The search status, search, link status and broken links handlers sent
core types straight to the client. Map them to dto types first so the
API docs can describe them without exposing core structs. The output is
byte-identical: backlinks and broken_incoming keep sending null for an
empty list, a follow-up changes that.
ToLinkStatus kept nil backlinks and broken_incoming, so
GET /api/pages/{id}/links serialized "backlinks": null and
"broken_incoming": null for a page nothing links to. Outgoing lists
already send []. Send [] for all four lists, which the generated API
docs mark as required.

Behavior-Change: GET /api/pages/{id}/links now returns [] instead of null for backlinks and broken_incoming when a page has none.
Annotate GET /api/search, /api/search/status, /api/pages/{id}/links and
/api/links/broken, regenerate docs/api, and drop their entries from the
route-coverage allowlist.

assertOperation learns object 200 responses and path parameters; the new
shape tests also pin the nested dto models. README and ADR 0014 no longer
list the search and links core types as undocumented.
@zorak1103
zorak1103 force-pushed the docs/api-search-links branch from 359c56f to c212d69 Compare November 2, 2026 17:20
@zorak1103
zorak1103 marked this pull request as ready for review November 2, 2026 18:02
@zorak1103
zorak1103 requested a review from perber as a code owner November 2, 2026 18:02
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.

1 participant