Description
GET /contracts lists every indexed contract, and GET /contracts/{id}/events and GET /contracts/{id}/export both operate on one contract — but there is no way to fetch a single contract's own record. A client that has a contract ID and wants its event count, ledger range and last-activity timestamp has to page through the whole /contracts list and filter client-side, or issue an events query and derive the fields itself.
Add GET /contracts/{id}, returning the same ContractSummary the list endpoint already produces, for one contract.
What "done" looks like
GET /contracts/{id} returns the contract's summary — ID, event count, first and last ledger, last activity — in the same shape and field names the list endpoint emits for its elements. A client parsing one should parse the other with no special-casing.
- A contract ID that is well-formed but has no indexed events returns
404, not an empty-valued 200. "No such contract here" and "a contract with zero events" are not distinguishable in this store, and 404 is the honest answer.
- A malformed contract ID returns
400 with the structured error envelope, consistent with the other {id} routes.
- The route respects the same scope/tenancy rules as
GET /contracts/{id}/events, so a scoped key cannot read a contract outside its watch list.
- ETag and cache headers behave consistently with the list endpoint.
Implementation guidance
- The route goes in internal/api/server.go beside the existing
/contracts/{id}/… registrations. Note there are two registration sites — the public tree and the scoped tree around line 412 — and the new route belongs in both, matching how /contracts/{id}/events is registered.
- The store already has the aggregation behind
ListContracts; prefer adding a single-contract query in internal/store/postgres.go over fetching a one-element page, so the index is used and no LIMIT/OFFSET machinery is involved. Add the method to the Store interface and implement it for both Postgres and SQLite — an interface method implemented on only one backend breaks the conformance suite.
- Return
store.ErrNotFound from the store and map it in the handler; do not invent a second not-found error.
- Update the OpenAPI spec in api/. The route-coverage test walks the live chi router and will fail if a registered route has no spec entry — this is the check most likely to catch you out, so run it early.
Expectations
- Handler tests: success, unknown contract → 404, malformed ID → 400, scoped-key denial.
- A store conformance test so both backends are covered by the same assertions.
- Postgres integration test proving the summary fields match what
ListContracts reports for the same contract — these must not drift.
- README endpoint table updated.
go build ./..., make test, make lint pass.
Complexity: Medium (150 points). One endpoint, but it touches the store interface, both backends, the scoped route tree and the OpenAPI spec.
Description
GET /contractslists every indexed contract, andGET /contracts/{id}/eventsandGET /contracts/{id}/exportboth operate on one contract — but there is no way to fetch a single contract's own record. A client that has a contract ID and wants its event count, ledger range and last-activity timestamp has to page through the whole/contractslist and filter client-side, or issue an events query and derive the fields itself.Add
GET /contracts/{id}, returning the sameContractSummarythe list endpoint already produces, for one contract.What "done" looks like
GET /contracts/{id}returns the contract's summary — ID, event count, first and last ledger, last activity — in the same shape and field names the list endpoint emits for its elements. A client parsing one should parse the other with no special-casing.404, not an empty-valued200. "No such contract here" and "a contract with zero events" are not distinguishable in this store, and404is the honest answer.400with the structured error envelope, consistent with the other{id}routes.GET /contracts/{id}/events, so a scoped key cannot read a contract outside its watch list.Implementation guidance
/contracts/{id}/…registrations. Note there are two registration sites — the public tree and the scoped tree around line 412 — and the new route belongs in both, matching how/contracts/{id}/eventsis registered.ListContracts; prefer adding a single-contract query in internal/store/postgres.go over fetching a one-element page, so the index is used and noLIMIT/OFFSETmachinery is involved. Add the method to theStoreinterface and implement it for both Postgres and SQLite — an interface method implemented on only one backend breaks the conformance suite.store.ErrNotFoundfrom the store and map it in the handler; do not invent a second not-found error.Expectations
ListContractsreports for the same contract — these must not drift.go build ./...,make test,make lintpass.Complexity: Medium (150 points). One endpoint, but it touches the store interface, both backends, the scoped route tree and the OpenAPI spec.