@thejob/jobs-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/dist/client.d.ts +154 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +211 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @thejob/jobs-client
|
|
2
|
+
|
|
3
|
+
Typed client for **thejob-jobs-service**. Published by the service that owns the
|
|
4
|
+
data, so consumers never hand-roll HTTP calls or redeclare job types.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { JobsClient } from '@thejob/jobs-client';
|
|
8
|
+
|
|
9
|
+
const jobs = new JobsClient({
|
|
10
|
+
baseUrl: process.env.JOBS_SERVICE_URL!,
|
|
11
|
+
apiKey: process.env.JOBS_SERVICE_API_KEY, // backend-to-backend
|
|
12
|
+
// token: userJwt, // user-scoped calls (bookmarks)
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const { results, total } = await jobs.listJobs({
|
|
16
|
+
search: 'angular developer',
|
|
17
|
+
workMode: ['remote', 'hybrid'],
|
|
18
|
+
category: ['Technology, Data & Digital'],
|
|
19
|
+
salary: { min: 50000 },
|
|
20
|
+
limit: 6,
|
|
21
|
+
});
|
|
22
|
+
// -> ?search=angular+developer&workMode[0]=remote&workMode[1]=hybrid
|
|
23
|
+
// &category[0]=Technology%2C+Data+%26+Digital&salary[min]=50000&limit=6
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Not yet wired to the service.** jobs-service still parses the older
|
|
27
|
+
> `filter[field]=value` form. The client emits the `qs` form above; migrating the
|
|
28
|
+
> service and its ~20 existing call sites is a separate, approved step.
|
|
29
|
+
|
|
30
|
+
## Methods
|
|
31
|
+
|
|
32
|
+
Twenty methods for twenty endpoints, one to one. No wrappers, no helpers.
|
|
33
|
+
|
|
34
|
+
| | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **Read** | `listJobs` · `getJob` · `listSimilarJobs` · `searchJobsV3` · `searchJobsMeili` |
|
|
37
|
+
| **Write** | `createJob` · `updateJob` · `deleteJob` · `closeJob` · `reopenJob` · `toggleJobFeatured` · `purgeJobCache` |
|
|
38
|
+
| **Bookmarks** | `addBookmark` · `removeBookmark` |
|
|
39
|
+
| **Expiry** | `listJobsDueForExpiryCheck` · `closeLapsedJobs` |
|
|
40
|
+
| **Indexing** | `getIndexingQuota` · `indexJobs` |
|
|
41
|
+
| **Admin** | `purgeAllJobsCache` · `health` |
|
|
42
|
+
|
|
43
|
+
There is no `countJobs`: a count is `listJobs({ ...filter, limit: 1 })` read off
|
|
44
|
+
`total`, which is one line at the call site and keeps what `total` means visible.
|
|
45
|
+
|
|
46
|
+
## Design
|
|
47
|
+
|
|
48
|
+
**One method per endpoint, never per use case.** Methods are named for the
|
|
49
|
+
resource and operation (`listJobs`, `closeJob`), not for why a caller wants the
|
|
50
|
+
data. A `getNewestJobsForADigest` would be the same mistake as an endpoint called
|
|
51
|
+
`/jobs/newest-for-digest`: one caller's intent baked into a shared contract.
|
|
52
|
+
Narrowing is the **filter's** job.
|
|
53
|
+
|
|
54
|
+
**Facets are flat and typed**, sitting next to paging exactly as they appear in
|
|
55
|
+
the URL, so the type mirrors the request it produces. A typo in a declared facet
|
|
56
|
+
is a compile error rather than a filter that saves, renders, and silently matches
|
|
57
|
+
nothing.
|
|
58
|
+
|
|
59
|
+
**Encoding is `qs` with its standard defaults** — arrays as `key[0]`, objects as
|
|
60
|
+
`key[nested]`. Because arrays are a *structure* rather than a delimited string, a
|
|
61
|
+
comma inside a value is just a character: no escaping, and no delimiter
|
|
62
|
+
convention left for a consumer to get wrong. (7 of the 9 job categories in use
|
|
63
|
+
contain a comma.)
|
|
64
|
+
|
|
65
|
+
Query strings are built by `buildQuerySuffix` from
|
|
66
|
+
[`@thejob/util/query`](../../../thejob-util/src/query), shared with every other
|
|
67
|
+
service client. It adds the one thing `qs` cannot know — which inputs must
|
|
68
|
+
produce **no parameter at all**. `qs` drops `undefined` and `[]`, but would
|
|
69
|
+
happily emit `workMode=`, `skills[0]=`, or a key with untrimmed spaces, each of
|
|
70
|
+
which matches nothing while looking correctly configured. Blank values and empty
|
|
71
|
+
facets are dropped; `false` and `0` are kept, being meaningful.
|
|
72
|
+
|
|
73
|
+
**`total` is the filter's count, not the page size.** Six cards under "142 new
|
|
74
|
+
jobs" is honest; counting the cards makes `limit` silently change the headline.
|
|
75
|
+
|
|
76
|
+
## Types
|
|
77
|
+
|
|
78
|
+
`JobDoc`, `JobListQuery`, `JobPage`, `BookmarkResult` are the **canonical**
|
|
79
|
+
definitions, owned here and imported back by jobs-service, so there is exactly
|
|
80
|
+
one definition of each.
|
|
81
|
+
|
|
82
|
+
The client covers jobs and nothing else. Saved searches are user data and belong
|
|
83
|
+
to user-service; alert sweeps belong to notification-service. Neither has a
|
|
84
|
+
method here.
|
|
85
|
+
|
|
86
|
+
It returns jobs as the service stores them and does no presentation: no renaming
|
|
87
|
+
to a template's field names, no joining a location into one string, no formatting
|
|
88
|
+
a salary. A consumer that needs a narrower payload asks for it with `fields`;
|
|
89
|
+
how a job is *displayed* is the consumer's own decision, not a shared contract.
|
|
90
|
+
|
|
91
|
+
## Coverage
|
|
92
|
+
|
|
93
|
+
`client.test.ts` reads `src/routes/jobs.ts` and `src/app.ts`, collects every route
|
|
94
|
+
the service registers, then drives a fake `fetch` through all twenty methods and
|
|
95
|
+
asserts nothing is left over. It records what each method actually *requests*
|
|
96
|
+
rather than grepping the source, so it proves the method calls the path.
|
|
97
|
+
|
|
98
|
+
A client that covers only part of an API is how consumers end up hand-rolling
|
|
99
|
+
"just this one call" alongside it — the duplication this package exists to remove.
|
|
100
|
+
|
|
101
|
+
Two exclusions, both deliberate:
|
|
102
|
+
|
|
103
|
+
- **`/webhooks/*`** — called *into* jobs-service by dory and the data-processor
|
|
104
|
+
and authenticated by signature, not by a consumer holding this client.
|
|
105
|
+
- **`GET /`, `/login`, `/logout`, `/dashboard/api/*`** — the operations dashboard.
|
|
106
|
+
Cookie-gated, same-origin, browser-only, so an API-key client could not
|
|
107
|
+
authenticate against them anyway. These live in `src/ui/`, which the test does
|
|
108
|
+
not read.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JobsClient: a typed wrapper over the thejob-jobs-service HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* The whole API, one method per ENDPOINT — not one method per use case. A
|
|
5
|
+
* consumer should never construct a URL or encode a filter itself. If
|
|
6
|
+
* jobs-service can do it, this client exposes it.
|
|
7
|
+
*
|
|
8
|
+
* Methods are named after the resource and the operation (`listJobs`, `getJob`,
|
|
9
|
+
* `closeJob`), never after why a particular caller wants it. A method called
|
|
10
|
+
* `getNewestJobsForADigest` would be the same mistake as an endpoint called
|
|
11
|
+
* `/jobs/newest-for-digest`: the caller's intent leaking into a shared contract.
|
|
12
|
+
* Narrowing is the FILTER's job.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE: jobs, and nothing else. This service is a DATA SOURCE. Saved searches
|
|
15
|
+
* are user data (owner, label, alert cadence, send bookkeeping — not one field
|
|
16
|
+
* describes a job) and belong to user-service; deciding whom to mail and when
|
|
17
|
+
* belongs to notification-service. Neither has a place here, so neither has a
|
|
18
|
+
* method here.
|
|
19
|
+
*
|
|
20
|
+
* Auth: pass `apiKey` for backend-to-backend calls, `token` for user-scoped ones
|
|
21
|
+
* (bookmarks). Uses the global `fetch`.
|
|
22
|
+
*/
|
|
23
|
+
import type { BookmarkResult, JobDoc, JobListQuery, JobPage } from './types.js';
|
|
24
|
+
export interface JobsClientOptions {
|
|
25
|
+
/** Base URL of the service, e.g. "http://localhost:3081" (trailing slash trimmed). */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** Service API key, sent as `x-api-key` (backend-to-backend callers). */
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
/** Bearer token, sent as `Authorization: Bearer <token>` (user-scoped calls). */
|
|
30
|
+
token?: string;
|
|
31
|
+
/** Optional fetch implementation (for tests or non-global-fetch runtimes). */
|
|
32
|
+
fetch?: typeof fetch;
|
|
33
|
+
/** Extra headers applied to every request. */
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
/** Thrown for any non-2xx response. Carries the status and parsed body. */
|
|
37
|
+
export declare class JobsClientError extends Error {
|
|
38
|
+
readonly status: number;
|
|
39
|
+
readonly body: unknown;
|
|
40
|
+
constructor(message: string, status: number, body: unknown);
|
|
41
|
+
}
|
|
42
|
+
export declare class JobsClient {
|
|
43
|
+
private readonly baseUrl;
|
|
44
|
+
private readonly fetchImpl;
|
|
45
|
+
private readonly headers;
|
|
46
|
+
constructor(options: JobsClientOptions);
|
|
47
|
+
private request;
|
|
48
|
+
/**
|
|
49
|
+
* A GET with the query string built by the shared `@thejob/util/query`
|
|
50
|
+
* helper, which owns the rules about what must NOT be sent — a blank value or
|
|
51
|
+
* an empty array encodes to a term matching nothing, silently.
|
|
52
|
+
*/
|
|
53
|
+
private get;
|
|
54
|
+
/**
|
|
55
|
+
* Jobs matching a filter, with the filter's TOTAL alongside the page.
|
|
56
|
+
*
|
|
57
|
+
* `total` is the count for the filter, not the number returned: a digest that
|
|
58
|
+
* shows six cards under "142 new jobs" is honest, whereas counting the cards
|
|
59
|
+
* makes `limit` silently change the headline.
|
|
60
|
+
*
|
|
61
|
+
* Pass `search` to rank by meaning (hybrid vector + FTS) instead of recency.
|
|
62
|
+
* For a count alone, pass `limit: 1` and read `total`.
|
|
63
|
+
*/
|
|
64
|
+
listJobs(query?: JobListQuery): Promise<JobPage>;
|
|
65
|
+
/** One job by shortId. Throws `JobsClientError` with status 404 if absent. */
|
|
66
|
+
getJob(shortId: string, fields?: string[]): Promise<JobDoc>;
|
|
67
|
+
/** Jobs similar to one job (vector neighbours). */
|
|
68
|
+
listSimilarJobs(shortId: string, query?: JobListQuery): Promise<JobPage>;
|
|
69
|
+
/** Query-understanding search: parsed facets applied as a hard filter. */
|
|
70
|
+
searchJobsV3(query?: JobListQuery): Promise<JobPage>;
|
|
71
|
+
/** Meilisearch-backed search. */
|
|
72
|
+
searchJobsMeili(query?: JobListQuery): Promise<JobPage>;
|
|
73
|
+
/**
|
|
74
|
+
* Create a job as the acting user (requires `token`).
|
|
75
|
+
*
|
|
76
|
+
* `draft: true` saves it as a Draft; anything else PUBLISHES it, which is the
|
|
77
|
+
* service's own default and is why this option exists rather than being left
|
|
78
|
+
* to the caller to append. Without it a "save as draft" button reaches
|
|
79
|
+
* `POST /jobs` with no flag and puts the job straight in front of applicants,
|
|
80
|
+
* with nothing to signal that the wrong thing happened.
|
|
81
|
+
*/
|
|
82
|
+
createJob(job: Partial<JobDoc>, options?: {
|
|
83
|
+
draft?: boolean;
|
|
84
|
+
}): Promise<JobDoc>;
|
|
85
|
+
/** Update a job the acting user owns. */
|
|
86
|
+
updateJob(shortId: string, patch: Partial<JobDoc>): Promise<JobDoc>;
|
|
87
|
+
deleteJob(shortId: string): Promise<unknown>;
|
|
88
|
+
closeJob(shortId: string): Promise<JobDoc>;
|
|
89
|
+
reopenJob(shortId: string): Promise<JobDoc>;
|
|
90
|
+
/**
|
|
91
|
+
* Admin: flip the featured flag, and say in WHICH markets.
|
|
92
|
+
*
|
|
93
|
+
* `markets` is not optional decoration. The service reads it as
|
|
94
|
+
* `Array.isArray(body.markets) ? body.markets : []`, so a call that omits it
|
|
95
|
+
* sends nothing and the job's featured markets are REPLACED WITH AN EMPTY
|
|
96
|
+
* LIST — the job stays flagged featured while surfacing in no market, with no
|
|
97
|
+
* error. This client omitted the argument entirely, so every caller silently
|
|
98
|
+
* cleared the targeting it meant to set.
|
|
99
|
+
*/
|
|
100
|
+
toggleJobFeatured(shortId: string, isFeatured: boolean, markets?: string[]): Promise<JobDoc>;
|
|
101
|
+
/** Admin: purge every locale variant of a job's page from the CDN. */
|
|
102
|
+
purgeJobCache(shortId: string): Promise<{
|
|
103
|
+
purged: boolean;
|
|
104
|
+
slug?: string;
|
|
105
|
+
}>;
|
|
106
|
+
addBookmark(shortId: string): Promise<BookmarkResult>;
|
|
107
|
+
removeBookmark(shortId: string): Promise<BookmarkResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Jobs due for an expiry re-check: undated published jobs old enough to be
|
|
110
|
+
* worth re-crawling, and not re-checked recently.
|
|
111
|
+
*
|
|
112
|
+
* The selection depends on when the service last re-checked each job, which is
|
|
113
|
+
* its own bookkeeping, so this is a purpose-built feed rather than something a
|
|
114
|
+
* caller could express as a `listJobs` filter.
|
|
115
|
+
*/
|
|
116
|
+
listJobsDueForExpiryCheck(query?: JobListQuery): Promise<JobPage>;
|
|
117
|
+
/**
|
|
118
|
+
* Run the dated auto-close sweep on demand.
|
|
119
|
+
*
|
|
120
|
+
* The in-process cron already runs this on a timer; this is the manual
|
|
121
|
+
* trigger for an operator or a test.
|
|
122
|
+
*/
|
|
123
|
+
closeLapsedJobs(): Promise<{
|
|
124
|
+
closed?: number;
|
|
125
|
+
[k: string]: unknown;
|
|
126
|
+
}>;
|
|
127
|
+
/** Remaining Google Indexing API budget. The service owns the count. */
|
|
128
|
+
getIndexingQuota(): Promise<{
|
|
129
|
+
used?: number;
|
|
130
|
+
limit?: number;
|
|
131
|
+
[k: string]: unknown;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* Submit jobs' canonical page URLs to Google's Indexing API so they are
|
|
135
|
+
* (re)crawled promptly. Over-budget requests are refused before Google is
|
|
136
|
+
* touched.
|
|
137
|
+
*/
|
|
138
|
+
indexJobs(shortIds: string[]): Promise<{
|
|
139
|
+
submitted?: number;
|
|
140
|
+
[k: string]: unknown;
|
|
141
|
+
}>;
|
|
142
|
+
/** Admin: purge the whole job cache (not one job — see `purgeJobCache`). */
|
|
143
|
+
purgeAllJobsCache(): Promise<{
|
|
144
|
+
purged?: boolean;
|
|
145
|
+
[k: string]: unknown;
|
|
146
|
+
}>;
|
|
147
|
+
/** Service health. */
|
|
148
|
+
health(): Promise<{
|
|
149
|
+
status: string;
|
|
150
|
+
db?: string;
|
|
151
|
+
uptime?: number;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JobsClient: a typed wrapper over the thejob-jobs-service HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* The whole API, one method per ENDPOINT — not one method per use case. A
|
|
5
|
+
* consumer should never construct a URL or encode a filter itself. If
|
|
6
|
+
* jobs-service can do it, this client exposes it.
|
|
7
|
+
*
|
|
8
|
+
* Methods are named after the resource and the operation (`listJobs`, `getJob`,
|
|
9
|
+
* `closeJob`), never after why a particular caller wants it. A method called
|
|
10
|
+
* `getNewestJobsForADigest` would be the same mistake as an endpoint called
|
|
11
|
+
* `/jobs/newest-for-digest`: the caller's intent leaking into a shared contract.
|
|
12
|
+
* Narrowing is the FILTER's job.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE: jobs, and nothing else. This service is a DATA SOURCE. Saved searches
|
|
15
|
+
* are user data (owner, label, alert cadence, send bookkeeping — not one field
|
|
16
|
+
* describes a job) and belong to user-service; deciding whom to mail and when
|
|
17
|
+
* belongs to notification-service. Neither has a place here, so neither has a
|
|
18
|
+
* method here.
|
|
19
|
+
*
|
|
20
|
+
* Auth: pass `apiKey` for backend-to-backend calls, `token` for user-scoped ones
|
|
21
|
+
* (bookmarks). Uses the global `fetch`.
|
|
22
|
+
*/
|
|
23
|
+
import { buildQuerySuffix } from '@thejob/util/query';
|
|
24
|
+
/** Thrown for any non-2xx response. Carries the status and parsed body. */
|
|
25
|
+
export class JobsClientError extends Error {
|
|
26
|
+
status;
|
|
27
|
+
body;
|
|
28
|
+
constructor(message, status, body) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.body = body;
|
|
32
|
+
this.name = 'JobsClientError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class JobsClient {
|
|
36
|
+
baseUrl;
|
|
37
|
+
fetchImpl;
|
|
38
|
+
headers;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
41
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
42
|
+
this.headers = { ...options.headers };
|
|
43
|
+
if (options.apiKey)
|
|
44
|
+
this.headers['x-api-key'] = options.apiKey;
|
|
45
|
+
if (options.token)
|
|
46
|
+
this.headers['authorization'] = `Bearer ${options.token}`;
|
|
47
|
+
if (!this.fetchImpl) {
|
|
48
|
+
throw new Error('No fetch implementation available; pass one via options.fetch');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ── plumbing ───────────────────────────────────────────────────────────────
|
|
52
|
+
async request(method, path, body) {
|
|
53
|
+
const init = { method, headers: { ...this.headers } };
|
|
54
|
+
if (body !== undefined) {
|
|
55
|
+
init.headers['content-type'] = 'application/json';
|
|
56
|
+
init.body = JSON.stringify(body);
|
|
57
|
+
}
|
|
58
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
const parsed = text ? JSON.parse(text) : null;
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const message = parsed?.error ?? `jobs-service ${res.status}`;
|
|
63
|
+
throw new JobsClientError(message, res.status, parsed);
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A GET with the query string built by the shared `@thejob/util/query`
|
|
69
|
+
* helper, which owns the rules about what must NOT be sent — a blank value or
|
|
70
|
+
* an empty array encodes to a term matching nothing, silently.
|
|
71
|
+
*/
|
|
72
|
+
get(path, q) {
|
|
73
|
+
const { fields, ...rest } = (q ?? {});
|
|
74
|
+
const params = { ...rest };
|
|
75
|
+
// ONE comma-joined param. `parseResponseFields` server-side reads
|
|
76
|
+
// `params.get('fields')` and splits on commas, so letting `qs` index it
|
|
77
|
+
// (`fields[0]=…`) means `.get('fields')` is null and the PROJECTION IS
|
|
78
|
+
// SILENTLY DROPPED — the caller gets every field of every row back, which
|
|
79
|
+
// looks like a working call with a much larger payload.
|
|
80
|
+
if (fields?.length)
|
|
81
|
+
params['fields'] = fields.join(',');
|
|
82
|
+
return this.request('GET', `${path}${buildQuerySuffix(params)}`);
|
|
83
|
+
}
|
|
84
|
+
// ── jobs: read ─────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Jobs matching a filter, with the filter's TOTAL alongside the page.
|
|
87
|
+
*
|
|
88
|
+
* `total` is the count for the filter, not the number returned: a digest that
|
|
89
|
+
* shows six cards under "142 new jobs" is honest, whereas counting the cards
|
|
90
|
+
* makes `limit` silently change the headline.
|
|
91
|
+
*
|
|
92
|
+
* Pass `search` to rank by meaning (hybrid vector + FTS) instead of recency.
|
|
93
|
+
* For a count alone, pass `limit: 1` and read `total`.
|
|
94
|
+
*/
|
|
95
|
+
listJobs(query = {}) {
|
|
96
|
+
return this.get('/jobs', query);
|
|
97
|
+
}
|
|
98
|
+
/** One job by shortId. Throws `JobsClientError` with status 404 if absent. */
|
|
99
|
+
getJob(shortId, fields) {
|
|
100
|
+
return this.get(`/jobs/${encodeURIComponent(shortId)}`, { fields });
|
|
101
|
+
}
|
|
102
|
+
/** Jobs similar to one job (vector neighbours). */
|
|
103
|
+
listSimilarJobs(shortId, query = {}) {
|
|
104
|
+
return this.get(`/jobs/${encodeURIComponent(shortId)}/similar`, query);
|
|
105
|
+
}
|
|
106
|
+
/** Query-understanding search: parsed facets applied as a hard filter. */
|
|
107
|
+
searchJobsV3(query = {}) {
|
|
108
|
+
return this.get('/jobs/search-v3', query);
|
|
109
|
+
}
|
|
110
|
+
/** Meilisearch-backed search. */
|
|
111
|
+
searchJobsMeili(query = {}) {
|
|
112
|
+
return this.get('/jobs/search-meili', query);
|
|
113
|
+
}
|
|
114
|
+
// ── jobs: write ────────────────────────────────────────────────────────────
|
|
115
|
+
/**
|
|
116
|
+
* Create a job as the acting user (requires `token`).
|
|
117
|
+
*
|
|
118
|
+
* `draft: true` saves it as a Draft; anything else PUBLISHES it, which is the
|
|
119
|
+
* service's own default and is why this option exists rather than being left
|
|
120
|
+
* to the caller to append. Without it a "save as draft" button reaches
|
|
121
|
+
* `POST /jobs` with no flag and puts the job straight in front of applicants,
|
|
122
|
+
* with nothing to signal that the wrong thing happened.
|
|
123
|
+
*/
|
|
124
|
+
createJob(job, options = {}) {
|
|
125
|
+
const suffix = options.draft ? buildQuerySuffix({ draft: true }) : '';
|
|
126
|
+
return this.request('POST', `/jobs${suffix}`, job);
|
|
127
|
+
}
|
|
128
|
+
/** Update a job the acting user owns. */
|
|
129
|
+
updateJob(shortId, patch) {
|
|
130
|
+
return this.request('PATCH', `/jobs/${encodeURIComponent(shortId)}`, patch);
|
|
131
|
+
}
|
|
132
|
+
deleteJob(shortId) {
|
|
133
|
+
return this.request('DELETE', `/jobs/${encodeURIComponent(shortId)}`);
|
|
134
|
+
}
|
|
135
|
+
closeJob(shortId) {
|
|
136
|
+
return this.request('POST', `/jobs/${encodeURIComponent(shortId)}/close`);
|
|
137
|
+
}
|
|
138
|
+
reopenJob(shortId) {
|
|
139
|
+
return this.request('POST', `/jobs/${encodeURIComponent(shortId)}/reopen`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Admin: flip the featured flag, and say in WHICH markets.
|
|
143
|
+
*
|
|
144
|
+
* `markets` is not optional decoration. The service reads it as
|
|
145
|
+
* `Array.isArray(body.markets) ? body.markets : []`, so a call that omits it
|
|
146
|
+
* sends nothing and the job's featured markets are REPLACED WITH AN EMPTY
|
|
147
|
+
* LIST — the job stays flagged featured while surfacing in no market, with no
|
|
148
|
+
* error. This client omitted the argument entirely, so every caller silently
|
|
149
|
+
* cleared the targeting it meant to set.
|
|
150
|
+
*/
|
|
151
|
+
toggleJobFeatured(shortId, isFeatured, markets = []) {
|
|
152
|
+
return this.request('PATCH', `/jobs/${encodeURIComponent(shortId)}/toggle-featured`, {
|
|
153
|
+
isFeatured,
|
|
154
|
+
markets,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Admin: purge every locale variant of a job's page from the CDN. */
|
|
158
|
+
purgeJobCache(shortId) {
|
|
159
|
+
return this.request('POST', `/jobs/${encodeURIComponent(shortId)}/purge-cache`);
|
|
160
|
+
}
|
|
161
|
+
// ── bookmarks (user-scoped; requires `token`) ──────────────────────────────
|
|
162
|
+
addBookmark(shortId) {
|
|
163
|
+
return this.request('PUT', `/jobs/${encodeURIComponent(shortId)}/bookmarks`);
|
|
164
|
+
}
|
|
165
|
+
removeBookmark(shortId) {
|
|
166
|
+
return this.request('DELETE', `/jobs/${encodeURIComponent(shortId)}/bookmarks`);
|
|
167
|
+
}
|
|
168
|
+
// ── expiry ─────────────────────────────────────────────────────────────────
|
|
169
|
+
/**
|
|
170
|
+
* Jobs due for an expiry re-check: undated published jobs old enough to be
|
|
171
|
+
* worth re-crawling, and not re-checked recently.
|
|
172
|
+
*
|
|
173
|
+
* The selection depends on when the service last re-checked each job, which is
|
|
174
|
+
* its own bookkeeping, so this is a purpose-built feed rather than something a
|
|
175
|
+
* caller could express as a `listJobs` filter.
|
|
176
|
+
*/
|
|
177
|
+
listJobsDueForExpiryCheck(query = {}) {
|
|
178
|
+
return this.get('/jobs/expiry-candidates', query);
|
|
179
|
+
}
|
|
180
|
+
// ── operations (admin / service key) ───────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* Run the dated auto-close sweep on demand.
|
|
183
|
+
*
|
|
184
|
+
* The in-process cron already runs this on a timer; this is the manual
|
|
185
|
+
* trigger for an operator or a test.
|
|
186
|
+
*/
|
|
187
|
+
closeLapsedJobs() {
|
|
188
|
+
return this.request('POST', '/jobs/close-lapsed');
|
|
189
|
+
}
|
|
190
|
+
/** Remaining Google Indexing API budget. The service owns the count. */
|
|
191
|
+
getIndexingQuota() {
|
|
192
|
+
return this.request('GET', '/jobs/index/quota');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Submit jobs' canonical page URLs to Google's Indexing API so they are
|
|
196
|
+
* (re)crawled promptly. Over-budget requests are refused before Google is
|
|
197
|
+
* touched.
|
|
198
|
+
*/
|
|
199
|
+
indexJobs(shortIds) {
|
|
200
|
+
return this.request('POST', '/jobs/index', { shortIds });
|
|
201
|
+
}
|
|
202
|
+
/** Admin: purge the whole job cache (not one job — see `purgeJobCache`). */
|
|
203
|
+
purgeAllJobsCache() {
|
|
204
|
+
return this.request('DELETE', '/jobs/cache');
|
|
205
|
+
}
|
|
206
|
+
/** Service health. */
|
|
207
|
+
health() {
|
|
208
|
+
return this.request('GET', '/health');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAElF,YAAY,EACV,cAAc,EACd,MAAM,EACN,SAAS,EACT,YAAY,EACZ,WAAW,EACX,OAAO,GACR,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAA0B,MAAM,aAAa,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire types of the thejob-jobs-service HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* CANONICAL definitions, owned by jobs-service and published here so consumers
|
|
5
|
+
* never redeclare them. marketing-service previously hand-wrote its own copies
|
|
6
|
+
* of these shapes and they drifted (the same value was `jobCount` in one place
|
|
7
|
+
* and `newJobCount` in another), which is the class of bug this package removes.
|
|
8
|
+
*
|
|
9
|
+
* jobs-service imports these back from the package, so there is exactly one
|
|
10
|
+
* definition of each shape.
|
|
11
|
+
*/
|
|
12
|
+
import type { ListQueryOptions } from '@thejob/util/query';
|
|
13
|
+
import type { InferType } from 'yup';
|
|
14
|
+
import type { JobFilterSchema, JobSchema } from '@thejob/schema';
|
|
15
|
+
/**
|
|
16
|
+
* Everything a list or search call can ask for.
|
|
17
|
+
*
|
|
18
|
+
* FLAT and TYPED: facets sit at the top level next to paging, exactly as they
|
|
19
|
+
* appear in the URL, so the type mirrors the request it produces. Declaring the
|
|
20
|
+
* known facets as real fields means a typo is a compile error rather than a
|
|
21
|
+
* filter that saves, renders, and silently matches nothing — the failure mode a
|
|
22
|
+
* loose `Record<string, string>` invites.
|
|
23
|
+
*
|
|
24
|
+
* Encoded with `qs` defaults, so arrays and nested objects are STRUCTURAL:
|
|
25
|
+
*
|
|
26
|
+
* workMode: ['remote', 'hybrid'] -> workMode[0]=remote&workMode[1]=hybrid
|
|
27
|
+
* salary: { min: 50000 } -> salary[min]=50000
|
|
28
|
+
* category: ['Technology, Data & Digital']
|
|
29
|
+
* -> category[0]=Technology%2C%20Data...
|
|
30
|
+
*
|
|
31
|
+
* A comma inside a value is just a character. Nothing needs escaping, and there
|
|
32
|
+
* is no delimiter convention to get wrong.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The closed vocabulary of facets a job query can filter on.
|
|
36
|
+
*
|
|
37
|
+
* Named separately, matching `UserFilter`, `GroupFilter` and `PageFilter`, so a
|
|
38
|
+
* consumer can talk about the filter on its own — a UI widget's state, or a
|
|
39
|
+
* saved search — without dragging paging along. `JobListQuery` is this plus the
|
|
40
|
+
* reserved params.
|
|
41
|
+
*/
|
|
42
|
+
export type JobFilter = Partial<InferType<typeof JobFilterSchema>>;
|
|
43
|
+
/**
|
|
44
|
+
* A jobs listing request: the facets, plus the reserved params.
|
|
45
|
+
*
|
|
46
|
+
* `ListQueryOptions` from `@thejob/util/query` supplies the reserved half
|
|
47
|
+
* (`search`, paging, sorting, `fields`), so this client cannot disagree with any
|
|
48
|
+
* other about what those mean. That block was spelled out here and, verbatim,
|
|
49
|
+
* in three sibling clients: four copies that agreed only by coincidence.
|
|
50
|
+
*
|
|
51
|
+
* The FACETS stay local, because they are this service's schema's business. The
|
|
52
|
+
* type is the plain intersection of the two halves rather than a shared
|
|
53
|
+
* `ListQuery<F>` wrapper: that wrapper was only ever `F & ListQueryOptions`,
|
|
54
|
+
* so it hid an `&` behind a longer spelling that every consumer had to alias
|
|
55
|
+
* anyway — and its bare form silently meant "no facets at all".
|
|
56
|
+
*
|
|
57
|
+
* Facets sit at the TOP LEVEL because that is what jobs-service parses
|
|
58
|
+
* (`workMode[0]=remote`), and `buildQuerySuffix` encodes the whole object as
|
|
59
|
+
* one flat query string — no `filter[...]` wrapper.
|
|
60
|
+
*/
|
|
61
|
+
export type JobListQuery = JobFilter & ListQueryOptions;
|
|
62
|
+
/**
|
|
63
|
+
* One place a job is offered.
|
|
64
|
+
*
|
|
65
|
+
* Named and exported rather than left inline on `JobDoc`, because consumers
|
|
66
|
+
* need to talk about a location on its own — marketing groups a digest by it,
|
|
67
|
+
* and hand-rolling the triple there is how a consumer ends up with a `country`
|
|
68
|
+
* where this says `countryCode`, matching nothing while typechecking fine.
|
|
69
|
+
*
|
|
70
|
+
* `countryCode` is uppercase ISO alpha-2 ("IN", "SE"); `city` and `state` are
|
|
71
|
+
* display names.
|
|
72
|
+
*/
|
|
73
|
+
export type JobLocation = NonNullable<InferType<typeof JobSchema>['locations']>[number];
|
|
74
|
+
/**
|
|
75
|
+
* A job as jobs-service returns it. DERIVED from `JobSchema`, never re-declared.
|
|
76
|
+
*
|
|
77
|
+
* The hand-written version drifted from the data it claimed to describe: it said
|
|
78
|
+
* `salary: { min, max, currency }` where a job carries `salaryRange` (with
|
|
79
|
+
* `compensationType` and `isNegotiable` besides), and its location was three
|
|
80
|
+
* optional fields where a real one has `country`, `address` and `geo`. Nothing
|
|
81
|
+
* caught it because the index signature below makes every field access
|
|
82
|
+
* typecheck, so an email template read `job.title` for months.
|
|
83
|
+
*
|
|
84
|
+
* `Partial` because a caller may narrow the response with `fields`; the index
|
|
85
|
+
* signature stays for the same reason — a projection can also carry computed
|
|
86
|
+
* extras (`score`, `id`) that the stored schema does not declare.
|
|
87
|
+
*/
|
|
88
|
+
export type JobDoc = Partial<InferType<typeof JobSchema>> & {
|
|
89
|
+
[field: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
/** A page of jobs plus how many matched the FILTER (not the page size). */
|
|
92
|
+
export interface JobPage {
|
|
93
|
+
results: JobDoc[];
|
|
94
|
+
total: number;
|
|
95
|
+
page?: number;
|
|
96
|
+
limit?: number;
|
|
97
|
+
}
|
|
98
|
+
export interface BookmarkResult {
|
|
99
|
+
bookmarked?: boolean;
|
|
100
|
+
[field: string]: unknown;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGjE;;;;;;;;;;;;;;;;;;GAkBG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,gBAAgB,CAAC;AAExD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC,SAAS,CAAC,OAAO,SAAS,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAExF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG;IAC1D,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B,CAAC;AAEF,2EAA2E;AAC3E,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thejob/jobs-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typed TypeScript/JavaScript client for the thejob-jobs-service HTTP API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@thejob/schema": "^2.1.9",
|
|
36
|
+
"@thejob/util": "file:.yalc/@thejob/util",
|
|
37
|
+
"qs": "^6.16.0",
|
|
38
|
+
"yup": "^1.7.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.19.19",
|
|
42
|
+
"typescript": "^5.9.3",
|
|
43
|
+
"vitest": "^4.1.9"
|
|
44
|
+
}
|
|
45
|
+
}
|