@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 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.
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAsBtD,2EAA2E;AAC3E,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAGtB;IACA;IAHlB,YACE,OAAe,EACC,MAAc,EACd,IAAa;QAE7B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAS;QAG7B,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,MAAM,OAAO,UAAU;IACJ,OAAO,CAAS;IAChB,SAAS,CAAe;IACxB,OAAO,CAAyB;IAEjD,YAAY,OAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;QACnD,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QACtC,IAAI,OAAO,CAAC,MAAM;YAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/D,IAAI,OAAO,CAAC,KAAK;YAAE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7E,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED,8EAA8E;IAEtE,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QACnE,MAAM,IAAI,GAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QACnE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACtB,IAAI,CAAC,OAAkC,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAC;YAC9E,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;QACjE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAY,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GACV,MAAoC,EAAE,KAAK,IAAI,gBAAgB,GAAG,CAAC,MAAM,EAAE,CAAC;YAC/E,MAAM,IAAI,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,MAAW,CAAC;IACrB,CAAC;IAED;;;;OAIG;IACK,GAAG,CAAI,IAAY,EAAE,CAAgB;QAC3C,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAiB,CAAC;QACtD,MAAM,MAAM,GAA4B,EAAE,GAAG,IAAI,EAAE,CAAC;QACpD,kEAAkE;QAClE,wEAAwE;QACxE,uEAAuE;QACvE,0EAA0E;QAC1E,wDAAwD;QACxD,IAAI,MAAM,EAAE,MAAM;YAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,GAAG,IAAI,GAAG,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAsB,EAAE;QAC/B,OAAO,IAAI,CAAC,GAAG,CAAU,OAAO,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,8EAA8E;IAC9E,MAAM,CAAC,OAAe,EAAE,MAAiB;QACvC,OAAO,IAAI,CAAC,GAAG,CAAS,SAAS,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,mDAAmD;IACnD,eAAe,CAAC,OAAe,EAAE,QAAsB,EAAE;QACvD,OAAO,IAAI,CAAC,GAAG,CAAU,SAAS,kBAAkB,CAAC,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAClF,CAAC;IAED,0EAA0E;IAC1E,YAAY,CAAC,QAAsB,EAAE;QACnC,OAAO,IAAI,CAAC,GAAG,CAAU,iBAAiB,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;IAED,iCAAiC;IACjC,eAAe,CAAC,QAAsB,EAAE;QACtC,OAAO,IAAI,CAAC,GAAG,CAAU,oBAAoB,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,8EAA8E;IAE9E;;;;;;;;OAQG;IACH,SAAS,CAAC,GAAoB,EAAE,UAA+B,EAAE;QAC/D,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,OAAO,IAAI,CAAC,OAAO,CAAS,MAAM,EAAE,QAAQ,MAAM,EAAE,EAAE,GAAG,CAAC,CAAC;IAC7D,CAAC;IAED,yCAAyC;IACzC,SAAS,CAAC,OAAe,EAAE,KAAsB;QAC/C,OAAO,IAAI,CAAC,OAAO,CAAS,OAAO,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACtF,CAAC;IAED,SAAS,CAAC,OAAe;QACvB,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,QAAQ,CAAC,OAAe;QACtB,OAAO,IAAI,CAAC,OAAO,CAAS,MAAM,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpF,CAAC;IAED,SAAS,CAAC,OAAe;QACvB,OAAO,IAAI,CAAC,OAAO,CAAS,MAAM,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;OASG;IACH,iBAAiB,CACf,OAAe,EACf,UAAmB,EACnB,UAAoB,EAAE;QAEtB,OAAO,IAAI,CAAC,OAAO,CAAS,OAAO,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,kBAAkB,EAAE;YAC3F,UAAU;YACV,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,aAAa,CAAC,OAAe;QAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAClF,CAAC;IAED,8EAA8E;IAE9E,WAAW,CAAC,OAAe;QACzB,OAAO,IAAI,CAAC,OAAO,CAAiB,KAAK,EAAE,SAAS,kBAAkB,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAC/F,CAAC;IAED,cAAc,CAAC,OAAe;QAC5B,OAAO,IAAI,CAAC,OAAO,CACjB,QAAQ,EACR,SAAS,kBAAkB,CAAC,OAAO,CAAC,YAAY,CACjD,CAAC;IACJ,CAAC;IAED,8EAA8E;IAE9E;;;;;;;OAOG;IACH,yBAAyB,CAAC,QAAsB,EAAE;QAChD,OAAO,IAAI,CAAC,GAAG,CAAU,yBAAyB,EAAE,KAAK,CAAC,CAAC;IAC7D,CAAC;IAED,8EAA8E;IAE9E;;;;;OAKG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACpD,CAAC;IAED,wEAAwE;IACxE,gBAAgB;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;IAClD,CAAC;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAkB;QAC1B,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,4EAA4E;IAC5E,iBAAiB;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC/C,CAAC;IAED,sBAAsB;IACtB,MAAM;QACJ,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IACxC,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ export { JobsClient, JobsClientError, type JobsClientOptions } from './client.js';
2
+ export type { BookmarkResult, JobDoc, JobFilter, JobListQuery, JobLocation, JobPage, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { JobsClient, JobsClientError } from './client.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }