Repository navigation
feat(sdk): add Go client SDK with full API-tree parity #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 54 commits
834a093
3089c71
61b436f
28eb1cf
b66413a
38b2397
5a480bf
fc6346e
e783f5b
6e166da
4b199b6
978451c
3a26401
10cb253
cade9d7
791c198
478b215
a841f1a
6a4e189
c88d1b1
697e978
c2c1021
0ee1734
51d8b45
4ee4f1f
2d043da
25e36cc
1158ff3
0a46251
9da52dd
4cc229a
6d29538
7b85e06
d878791
59b27bb
5d26c34
90d59ef
c460c02
941b097
c538ee3
167e16d
73edcbe
3ed1698
3831741
ed2eace
20bb752
527dfea
059ad16
0b8e450
29c6b6b
3947df3
48a24e8
6c94796
e17152f
50bdc2f
23c58e2
4094272
f778e41
ce066c9
ff35c8b
607d000
654924a
edbc0b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,11 +10,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | |
|
|
||
| ### Added | ||
|
|
||
| - **Go SDK — official Go client with full API-tree parity against the TypeScript SDK** (`clients/go/` (new nested Go module), `tests/conformance/conformance_ts.mjs` (new), `docs/src/content/docs/sdk/go/{index,queries,streaming,pipes,admin,reference}.md` (new), `docs/src/content/docs/sdk/index.mdx`, `docs/src/config/sidebar.ts`, `Makefile`, `.github/workflows/ci.yml`, `AGENTS.md`, `CONTRIBUTING.md`, `README.md`, `SUPPORT.md`, `docs/src/content/docs/{development,architecture,getting-started,why-wavehouse,404,api,claude-code,deployment}.md`, `docs/src/content/docs/{access-control,pipes,reverse-proxy}.mdx`, `docs/src/content/docs/sdk/{queries,streaming,pipes,admin,reference}.md`, `docs/src/content/docs/index.mdx`): install with `go get github.com/Wave-RF/WaveHouse/clients/go` (package `wavehouse`; a *nested* Go module with its own `go.mod`, invisible to root `go list` — hence the dedicated make targets). Zero third-party runtime dependencies, `context.Context`-first, generics for typed rows (`FetchTyped[Row]`, `Fetch[Row]`, `SQL[Row]`), an immutable chainable query builder with keyset pagination, real-time SSE streaming with reconnect/backfill (`Stream`, `LiveQuery`), admin namespaces (Schema/Policy/Pipes/DLQ/Sys), per-client `Headers` applied to REST and SSE alike (the Go analog of the TypeScript SDK's `options.headers`, and how an operator sends `X-Operator-Key`), and a `wavehouse-codegen` CLI that generates row structs from `/v1/ops/schema`. Wire-format parity is enforced by a shared fixture (`clients/go/testdata/wire_cases.json`) replayed by two conformance runners — Go (`conformance_test.go`, in `make test-go-sdk`) and TS (`make test-conformance-ts`) — both run by CI's unit job and local `make ci`. New make targets: `test-go-sdk` (with `-race`), `test-go-sdk-e2e` (live server, `WAVEHOUSE_URL`/`WAVEHOUSE_AUTH`), `test-conformance-ts`, `lint-go-sdk`, `verify-go-sdk`; the `test`/`lint`/`fix` aggregates cover the nested module. Docs ship as a six-page tree under `/sdk/go/`. Releases ride the tag-driven scheme already in place: `make release-sdk-go` cuts a `clients/go/vX.Y.Z` tag (`scripts/release.sh`), which the Go module proxy serves directly — no publish workflow needed. | ||
| - **"Was this page helpful?" feedback widget on every docs page** (`docs/src/components/PageFeedback.astro` (new), `docs/src/components/Footer.astro`): a thumbs-up / thumbs-down vote below the page content, captured to PostHog as `docs_feedback` with `{ helpful, page }`. It renders from `Footer.astro`'s sidebar branch — the same indirection the Cloud CTA uses — rather than a per-page import or frontmatter flag, so every content page gets it automatically, including ones not written yet; it sits *below* the Cloud CTA on the pages that carry one, and splash pages (the homepage and 404) take the other footer branch and never render it. One vote per page per visitor: the choice is remembered in `localStorage` keyed by pathname, and a revisit renders the thanks message instead of re-prompting (storage is a nicety, not the record — a browser with storage disabled still votes). | ||
| - **Settings-directory validation — `wavehouse validate [dir]`** (`internal/settings/` (new: `settings.go`, `validate.go`, `decode.go`, `finding.go`, + tests), `cmd/wavehouse/validate.go` (new, + tests), `cmd/wavehouse/main.go`): first piece of the file-based control plane (settings live in a directory of JSON documents — `roles.json`, `policies.json`, `pipes.json`, `config.json` — that a running instance will hot-reload; this change is validation-only — boot loading and reload wiring land separately). `settings.Validate(dir)` is the single gate every consumer of the directory runs: deliberately pure (no network, no ClickHouse — table/column existence stays with schema discovery, per Bring-Your-Own-Schema), and it collects **all** findings in one pass instead of failing on the first. Checks, layered: the directory holds exactly the four files (a missing file is an error — an empty document is `{}`, so absence always means deletion or a wrong path; any unexpected entry — file or directory — is an error so a typoed `polices.json` or a stray backup can't be silently ignored; dot-prefixed entries are the one carve-out, since erroring on vim swap files or the `..data` machinery Kubernetes ConfigMap mounts publish through would break hand editing and the cloud fan-out's mount pattern alike); strict JSON syntax (unknown fields rejected — the JSON form of the retired-config-key trap; empty/truncated files rejected, never read as an empty document; a leading UTF-8 byte order mark named as such instead of surfacing as a cryptic invalid-character error; a directory, unreadable file, or non-regular file (a FIFO would hang the read forever waiting for a writer; a stat gate rejects it — following symlinks, so Kubernetes ConfigMap mounts' symlink layout still passes) squatting on a settings filename named as the one real problem, not double-reported as "missing"; a top-level `null` rejected — the one well-formed document that decodes into a zero value without error, so it would silently read as "no settings"; trailing content rejected; duplicated object keys detected by a token-level pass, since `encoding/json` silently keeps the last copy); per-file shape rules (role names non-empty/unique, pipe names/SQL/param types, `config.json` bounds mirroring boot-config validation — its sections are the *tenant-owned* behavioral tunables (dedupe id_field/require_id plus per-table overrides under `dedupe.tables` — each entry overrides only the fields it names, resolving table → global → compiled default per field, so the effective id_field can never be empty — an explicit empty, whitespace-only, or whitespace-padded id_field is rejected at both levels, since an exact-match JSON key lookup would silently miss every row ([#222](https://github.com/Wave-RF/WaveHouse/issues/222)'s shape, unblocked by the file design since table names are runtime-resolved like policy grants); query default_max_rows, schema refresh_interval, CORS origins); platform-owned knobs like the SSE keepalives deliberately stay boot config); and cross-file referential integrity (every role a policy grant, `default_role`/`admin_role`, or pipe allowlist references must be declared in `roles.json`; an empty role string in a grant or allowlist is named as such — it matches no request and authorizes nobody). Warnings don't invalidate: a grant scoping the admin role (an unconditional bypass — dead config), `default_role` = admin, and a `default` on a required pipe parameter are flagged but legal. An empty `policies.json` means no policy — fail closed, matching deleted-policy semantics — and draws a warning naming the total lockout, so it announces itself at validation time instead of one 403 at a time. The CLI (`cmd/wavehouse/validate.go`, following the `health` subcommand pattern) takes the directory as an argument or from `WH_SETTINGS_DIR`, prints findings, and exits 0/1/2 (valid/invalid/usage) so CI and operators can gate config changes before they reach a running instance. The dispatch in `main.go` also grows `help` and `version` subcommands, and an unknown command is now a usage error instead of silently falling through and starting the server (`wavehouse validat` booting a listener is not a typo anyone wants); each subcommand parses its arguments with a stdlib `flag.FlagSet`, so `wavehouse <command> -h` prints command-specific help and a stray flag or argument is a usage error rather than being silently swallowed. `WH_SETTINGS_DIR` has a single authority: `config.EnvSettingsDir`, with a reflection test pinning the `settings.dir` struct tag to it. The directory's location joins boot config as `settings.dir` (`WH_SETTINGS_DIR`; `internal/config/config.go`, `config.yaml`, `docs/src/content/docs/configuration.mdx`) — boot-tier by necessity, since it's the pointer the reload machinery follows; no default, same silent-misconfiguration reasoning as `policy.file_path`. | ||
|
|
||
| - **Docs-site analytics for search, code copies, 404s, docs section, and live-demo connectivity** (`docs/src/components/DocsTracking.astro` (new), `docs/src/components/{PostHog,Footer,LiveDemo}.astro`): the site tracked its own CTAs but nothing a reader did on the way to one, so the questions that decide what to write next — what people search for and *don't* find, which snippets get copied, which dead links keep getting followed — had no data behind them. `docs_search` fires a second after the query settles rather than once per keystroke, carrying `query` and `result_count` read off Pagefind's own results message (the rendered list is capped at its page size, so counting the DOM would under-report); `result_count: 0` is the event worth having. `code_copied` (`page`, `language`) watches Expressive Code's copy buttons from the document rather than re-binding every code block on every navigation — the hero's install chip is not an EC block and keeps its own `hero_install_copied`. `docs_404` (`path`, `referrer`) turns broken inbound links into a list instead of a hunch. A `doc_section` property (the first path segment, `home` for `/`) puts every event in a docs area without each tracker carrying its own copy; it's stamped at capture time by a `before_send` hook in `posthog.init()` rather than `register()`, because a queued `register()` replays only after init has already captured the first hard-load `$pageview` — which would then carry the previous visit's persisted value — and `history_change` navigations update the URL before capture fires, so reading `location` in the hook is always current. `live_demo_connected` fires once per mount when the hero's SSE feed comes up rather than on its first row — named for what it measures (the demo backend answered), since a quiet minute on the repo is not a disengaged reader. The three site-wide trackers share one new `DocsTracking.astro` rendered from the footer (like `MermaidZoom` / `ScrollHints`) and delegate from `document`, since Pagefind, Expressive Code, and the 404 route all own their own markup — some of it created after page load. | ||
|
|
||
| ### Fixed | ||
|
|
||
| - **Go SDK client-side stream filters compare timestamps as instants, not as text** (`clients/go/stream.go`, `clients/go/stream_test.go`, `docs/src/content/docs/sdk/go/streaming.md`): the server canonicalizes every top-level `DateTime`/`DateTime64` value to RFC 3339 UTC before publishing (#402), so a payload reads `2026-06-21T04:00:00Z` while a caller's filter constant may name the same instant as `2026-06-21T06:00:00+02:00`. Compared as text those disagree in both directions — lexically the payload sorts *below* the constant, so `OpGte` withheld a row that was chronologically equal. Both sides now parse as instants, mirroring the server's row-filter rule for DateTime columns. Only unambiguous spellings count (RFC 3339 with an explicit offset or `Z`): a zone-less constant names an instant only relative to the column's declared timezone, which a stream subscriber doesn't have, so reading it as UTC would move the instant. Ordering an instant against a non-instant now fails closed rather than falling back to text comparison. Also fixes a missing column matching the literal string `"<nil>"` through the equality fallback. The TypeScript SDK's `matchesFilters` has the same text-comparison behavior and needs the same change for parity. | ||
|
|
||
| - **TypeScript SDK documentation corrections found while writing the Go SDK's parity docs** (`docs/src/content/docs/sdk/{reference,queries,streaming}.md`): the error-code table described `401` as "missing or invalid JWT" when a *missing* token is actually evaluated as `default_role` — succeeding or denied with `403`, never `401` (`internal/auth/auth.go`, `internal/api/errors.go`) — and only a present-but-invalid or expired token yields `401`. `.aggregate()` was documented as accepting a "custom fn", but the server enforces an allowlist (`internal/query/builder.go`); the allowed set is now listed. Live queries gained a caution for a real footgun: the backfill dedup boundary comes from the fetched rows' `received_timestamp`, so a `.select(...)` projection omitting that column silently disables dedup and delivers overlap-window events twice. No SDK code changed — these were pre-existing gaps between the TS docs and the server's behavior. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Correct the SDK page file extensions. Line 23 identifies Change the suffix to 🧰 Tools🪛 LanguageTool[style] ~23-~23: Consider using the typographical ellipsis character here instead. (ELLIPSIS) |
||
|
|
||
| ## [0.1.0] - 2026-08-19 | ||
|
|
||
| The first public release. Everything below shipped in it — the sections are grouped the way Keep a Changelog asks for, but since there is no previous release to compare against, a reader upgrading from nothing can treat the whole file as "Added". The date is the intended cut date; correct it if tagging slips, and move anything merged in between up from `## Unreleased`. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.