@thejob/common-client
v1.0.1
Published
Typed TypeScript/JavaScript client for the thejob-common-service HTTP API.
Downloads
388
Readme
@thejob/common-client
Typed client for thejob-common-service: the shared taxonomy lookups and the feedback inbox.
import { CommonClient } from '@thejob/common-client';
const common = new CommonClient({ baseUrl: process.env.COMMON_SERVICE_URL! });Two surfaces, two auth postures
| surface | auth |
|---|---|
| /skills, /languages, /locations | public |
| POST /feedback | any signed-in user's Bearer token |
| GET/PATCH /feedback… | admin token |
There is no API-key path, so apiKey alone reaches nothing gated. Pass
token.
The API
| method | endpoint |
|---|---|
| listSkills(query?) | GET /skills |
| listLanguages(query?) | GET /languages |
| autocompleteLocations(search, mode?) | GET /locations |
| submitFeedback(body) | POST /feedback |
| listFeedback(query?) | GET /feedback |
| getFeedback(id) | GET /feedback/:id |
| updateFeedbackStatus(id, status) | PATCH /feedback/:id |
| health() | GET /health |
The filter is nested, not flat
Unlike @thejob/jobs-client, this service reads facets from a filter[name]
wrapper. The client flattens them for you:
await common.listSkills({ filter: { category: ['tech', 'design'] } });
// -> ?filter[category]=tech,designThis is not cosmetic. The service parses filters with /^filter\[(.+)\]$/,
which is greedy, so a naively nested filter[category][0]=tech is read as a
field literally named category][0. That is a query for a field no document
has: 200 OK, zero rows, no error anywhere. The client flattens every value to a
single string before qs can nest it, and a test asserts no [0] appears.
Two consequences worth knowing:
- A comma-bearing value throws. The service splits on commas
unconditionally with no escape convention, so
"Banking, Investments"would shred into two terms matching nothing. Failing loudly is the only honest option until the service grows an escape. - Feedback facets are single-valued.
GET /feedbackreadstypeandstatuswithparams.get(), not the comma-splitting parser, so there is no way to ask for two types in one call. It also reads only those two keys; the client rejects anything else rather than letting an ignored key return the unfiltered page.
/locations fails soft, and callers must know
This endpoint never errors. A missing upstream Geoapify key, a blank query,
or an upstream outage all return { results: [] } with a 200, which is
indistinguishable from "no place matched". An empty result is not evidence the
place does not exist, and an autocomplete that silently stops working produces
no client-side signal at all.
Feedback attribution cannot be spoofed
submitFeedback takes only type, message, email and pageUrl. The user
id, email, name, user agent and locale are attached server-side from the
verified token and request headers, which is exactly why they are absent from
FeedbackSubmission.
Note that updateFeedbackStatus returns 404, not 400, for an unknown
status, so a typo reads as a missing record.
Development
pnpm build # tsc -p tsconfig.json
pnpm test # vitest runThe test suite reads the service's own route files and asserts this client has a method for every endpoint they register, so an endpoint added without a client method fails here.
