@ttctl/core 0.0.0 → 0.1.0-rc.1
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 +49 -9
- package/dist/__generated__/gateway.d.ts +4546 -0
- package/dist/__generated__/gateway.d.ts.map +1 -0
- package/dist/__generated__/gateway.js +9 -0
- package/dist/__generated__/gateway.js.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
- package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
- package/dist/__generated__/talent-profile.d.ts +1397 -0
- package/dist/__generated__/talent-profile.d.ts.map +1 -0
- package/dist/__generated__/talent-profile.js +9 -0
- package/dist/__generated__/talent-profile.js.map +1 -0
- package/dist/__generated__/zod-schemas.d.ts +2895 -0
- package/dist/__generated__/zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/zod-schemas.js +3121 -0
- package/dist/__generated__/zod-schemas.js.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.js +196 -0
- package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
- package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
- package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/data.js +230 -0
- package/dist/__tests__/fixtures/profile/data.js.map +1 -0
- package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
- package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/index.js +10 -0
- package/dist/__tests__/fixtures/profile/index.js.map +1 -0
- package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
- package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/types.js +4 -0
- package/dist/__tests__/fixtures/profile/types.js.map +1 -0
- package/dist/auth/errors.d.ts +82 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +68 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth.d.ts +192 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +294 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +212 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +349 -0
- package/dist/config.js.map +1 -0
- package/dist/configLock.d.ts +50 -0
- package/dist/configLock.d.ts.map +1 -0
- package/dist/configLock.js +88 -0
- package/dist/configLock.js.map +1 -0
- package/dist/configWriter.d.ts +97 -0
- package/dist/configWriter.d.ts.map +1 -0
- package/dist/configWriter.js +687 -0
- package/dist/configWriter.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-switch.d.ts +161 -0
- package/dist/kill-switch.d.ts.map +1 -0
- package/dist/kill-switch.js +235 -0
- package/dist/kill-switch.js.map +1 -0
- package/dist/lib/date.d.ts +58 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +104 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/lib/diagnostic-log.d.ts +159 -0
- package/dist/lib/diagnostic-log.d.ts.map +1 -0
- package/dist/lib/diagnostic-log.js +186 -0
- package/dist/lib/diagnostic-log.js.map +1 -0
- package/dist/lib/package-version.d.ts +19 -0
- package/dist/lib/package-version.d.ts.map +1 -0
- package/dist/lib/package-version.js +38 -0
- package/dist/lib/package-version.js.map +1 -0
- package/dist/lib/redact.d.ts +153 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +207 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/text.d.ts +14 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/lib/text.js +21 -0
- package/dist/lib/text.js.map +1 -0
- package/dist/lib/wire-shape.d.ts +131 -0
- package/dist/lib/wire-shape.d.ts.map +1 -0
- package/dist/lib/wire-shape.js +376 -0
- package/dist/lib/wire-shape.js.map +1 -0
- package/dist/onepassword.d.ts +29 -0
- package/dist/onepassword.d.ts.map +1 -0
- package/dist/onepassword.js +112 -0
- package/dist/onepassword.js.map +1 -0
- package/dist/services/_shared/transport.d.ts +148 -0
- package/dist/services/_shared/transport.d.ts.map +1 -0
- package/dist/services/_shared/transport.js +102 -0
- package/dist/services/_shared/transport.js.map +1 -0
- package/dist/services/applications/index.d.ts +210 -0
- package/dist/services/applications/index.d.ts.map +1 -0
- package/dist/services/applications/index.js +240 -0
- package/dist/services/applications/index.js.map +1 -0
- package/dist/services/availability/index.d.ts +254 -0
- package/dist/services/availability/index.d.ts.map +1 -0
- package/dist/services/availability/index.js +310 -0
- package/dist/services/availability/index.js.map +1 -0
- package/dist/services/contracts/index.d.ts +132 -0
- package/dist/services/contracts/index.d.ts.map +1 -0
- package/dist/services/contracts/index.js +211 -0
- package/dist/services/contracts/index.js.map +1 -0
- package/dist/services/engagements/index.d.ts +504 -0
- package/dist/services/engagements/index.d.ts.map +1 -0
- package/dist/services/engagements/index.js +613 -0
- package/dist/services/engagements/index.js.map +1 -0
- package/dist/services/jobs/index.d.ts +490 -0
- package/dist/services/jobs/index.d.ts.map +1 -0
- package/dist/services/jobs/index.js +753 -0
- package/dist/services/jobs/index.js.map +1 -0
- package/dist/services/payments/index.d.ts +415 -0
- package/dist/services/payments/index.d.ts.map +1 -0
- package/dist/services/payments/index.js +636 -0
- package/dist/services/payments/index.js.map +1 -0
- package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
- package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
- package/dist/services/profile/__tests__/fixtures.js +176 -0
- package/dist/services/profile/__tests__/fixtures.js.map +1 -0
- package/dist/services/profile/basic/index.d.ts +390 -0
- package/dist/services/profile/basic/index.d.ts.map +1 -0
- package/dist/services/profile/basic/index.js +1007 -0
- package/dist/services/profile/basic/index.js.map +1 -0
- package/dist/services/profile/certifications/index.d.ts +74 -0
- package/dist/services/profile/certifications/index.d.ts.map +1 -0
- package/dist/services/profile/certifications/index.js +169 -0
- package/dist/services/profile/certifications/index.js.map +1 -0
- package/dist/services/profile/education/index.d.ts +73 -0
- package/dist/services/profile/education/index.d.ts.map +1 -0
- package/dist/services/profile/education/index.js +168 -0
- package/dist/services/profile/education/index.js.map +1 -0
- package/dist/services/profile/employment/index.d.ts +111 -0
- package/dist/services/profile/employment/index.d.ts.map +1 -0
- package/dist/services/profile/employment/index.js +202 -0
- package/dist/services/profile/employment/index.js.map +1 -0
- package/dist/services/profile/external/index.d.ts +219 -0
- package/dist/services/profile/external/index.d.ts.map +1 -0
- package/dist/services/profile/external/index.js +560 -0
- package/dist/services/profile/external/index.js.map +1 -0
- package/dist/services/profile/index.d.ts +24 -0
- package/dist/services/profile/index.d.ts.map +1 -0
- package/dist/services/profile/index.js +26 -0
- package/dist/services/profile/index.js.map +1 -0
- package/dist/services/profile/industries/index.d.ts +130 -0
- package/dist/services/profile/industries/index.d.ts.map +1 -0
- package/dist/services/profile/industries/index.js +292 -0
- package/dist/services/profile/industries/index.js.map +1 -0
- package/dist/services/profile/portfolio/index.d.ts +352 -0
- package/dist/services/profile/portfolio/index.d.ts.map +1 -0
- package/dist/services/profile/portfolio/index.js +833 -0
- package/dist/services/profile/portfolio/index.js.map +1 -0
- package/dist/services/profile/resume/index.d.ts +60 -0
- package/dist/services/profile/resume/index.d.ts.map +1 -0
- package/dist/services/profile/resume/index.js +212 -0
- package/dist/services/profile/resume/index.js.map +1 -0
- package/dist/services/profile/reviews/index.d.ts +137 -0
- package/dist/services/profile/reviews/index.d.ts.map +1 -0
- package/dist/services/profile/reviews/index.js +431 -0
- package/dist/services/profile/reviews/index.js.map +1 -0
- package/dist/services/profile/shared.d.ts +127 -0
- package/dist/services/profile/shared.d.ts.map +1 -0
- package/dist/services/profile/shared.js +155 -0
- package/dist/services/profile/shared.js.map +1 -0
- package/dist/services/profile/skills/index.d.ts +212 -0
- package/dist/services/profile/skills/index.d.ts.map +1 -0
- package/dist/services/profile/skills/index.js +461 -0
- package/dist/services/profile/skills/index.js.map +1 -0
- package/dist/services/profile/visas/index.d.ts +74 -0
- package/dist/services/profile/visas/index.d.ts.map +1 -0
- package/dist/services/profile/visas/index.js +306 -0
- package/dist/services/profile/visas/index.js.map +1 -0
- package/dist/services/timesheet/index.d.ts +326 -0
- package/dist/services/timesheet/index.d.ts.map +1 -0
- package/dist/services/timesheet/index.js +324 -0
- package/dist/services/timesheet/index.js.map +1 -0
- package/dist/services/translations.d.ts +79 -0
- package/dist/services/translations.d.ts.map +1 -0
- package/dist/services/translations.js +136 -0
- package/dist/services/translations.js.map +1 -0
- package/dist/transport-resilience.d.ts +136 -0
- package/dist/transport-resilience.d.ts.map +1 -0
- package/dist/transport-resilience.js +247 -0
- package/dist/transport-resilience.js.map +1 -0
- package/dist/transport.d.ts +408 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +691 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -12
- package/index.js +0 -7
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 Oleksii PELYKH
|
|
3
|
+
import { buildDryRunPreview } from "../../transport.js";
|
|
4
|
+
import { callGatewayShared } from "../_shared/transport.js";
|
|
5
|
+
export class JobsError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
name = "JobsError";
|
|
8
|
+
constructor(code, message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Default values for {@link ListOptions} pagination fields when the
|
|
15
|
+
* caller does not specify them. Mirrors the pre-#138 hardcoded values
|
|
16
|
+
* in `JOBS_LIST_QUERY` (`page: 0, pageSize: 20` on the wire — `page:
|
|
17
|
+
* 1, perPage: 20` user-facing). Exposed so tests can assert against
|
|
18
|
+
* the same constants the production code uses.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_PAGE = 1;
|
|
21
|
+
export const DEFAULT_PER_PAGE = 20;
|
|
22
|
+
// ---------------------------------------------------------------------
|
|
23
|
+
// GraphQL operation strings (full-document queries — no APQ pinning)
|
|
24
|
+
//
|
|
25
|
+
// Mirror the captured documents in
|
|
26
|
+
// `../research/graphql/gateway/operations/mobile/`, but with selection
|
|
27
|
+
// sets trimmed to the shape this service surfaces. Operation NAMES are
|
|
28
|
+
// kept distinct from the captured names because (a) we trim aggressively
|
|
29
|
+
// and (b) the captured names refer to flows we don't implement (e.g.
|
|
30
|
+
// `InitialJobs` selects fragments unrelated to this service's contract).
|
|
31
|
+
// ---------------------------------------------------------------------
|
|
32
|
+
// Pagination variable types (issue #138):
|
|
33
|
+
//
|
|
34
|
+
// - `$page: Int` — nullable Int; server defaults to 0 when omitted.
|
|
35
|
+
// Verified empirically: BlogPosts, GetJobsForDashboard, and
|
|
36
|
+
// GetTalentReferralTrackers (in
|
|
37
|
+
// `research/graphql/gateway/operations/`) all declare `$page: Int`.
|
|
38
|
+
//
|
|
39
|
+
// - `$pageSize: PageSize` — CUSTOM SCALAR, NOT `Int`. Captured operations
|
|
40
|
+
// use the named `PageSize` scalar. The pre-#138 hardcoded literal
|
|
41
|
+
// `pageSize: 20` worked because GraphQL accepts integer literals for
|
|
42
|
+
// custom scalars without type-checking; once we extracted the value
|
|
43
|
+
// to a variable, the server validated and (correctly) rejected
|
|
44
|
+
// `Int` in a `PageSize`-typed position. The fix is to declare the
|
|
45
|
+
// variable with the actual server type. Verified empirically: live
|
|
46
|
+
// API returned HTTP 400 `Variable "$pageSize" of type "Int!" used
|
|
47
|
+
// in position expecting type "PageSize"` during E2E pre-merge
|
|
48
|
+
// verification — schema/contract validation rule caught it.
|
|
49
|
+
const JOBS_LIST_QUERY = `query JobsList($skills: [String!], $keywords: [String!], $excludeSkills: [String!], $excludeKeywords: [String!], $commitments: [JobCommitmentFilterEnum!], $workTypes: [JobWorkTypeSlug!], $estimatedLengths: [EstimatedLengthFilterEnum!], $notInterested: BooleanFilter, $saved: BooleanFilter, $sortTarget: String, $page: Int, $pageSize: PageSize) {
|
|
50
|
+
viewer {
|
|
51
|
+
__typename
|
|
52
|
+
id
|
|
53
|
+
eligibleJobs(
|
|
54
|
+
page: $page
|
|
55
|
+
pageSize: $pageSize
|
|
56
|
+
sortTarget: $sortTarget
|
|
57
|
+
skills: $skills
|
|
58
|
+
keywords: $keywords
|
|
59
|
+
excludeSkills: $excludeSkills
|
|
60
|
+
excludeKeywords: $excludeKeywords
|
|
61
|
+
commitmentsV2: $commitments
|
|
62
|
+
workTypesV2: $workTypes
|
|
63
|
+
estimatedLengths: $estimatedLengths
|
|
64
|
+
filter: { notInterested: $notInterested, saved: $saved }
|
|
65
|
+
) {
|
|
66
|
+
__typename
|
|
67
|
+
entities {
|
|
68
|
+
__typename
|
|
69
|
+
id
|
|
70
|
+
title
|
|
71
|
+
url
|
|
72
|
+
commitment { __typename slug }
|
|
73
|
+
workType { __typename slug }
|
|
74
|
+
specialization { __typename title }
|
|
75
|
+
expectedHours
|
|
76
|
+
maxRate
|
|
77
|
+
startDate
|
|
78
|
+
postedWhen
|
|
79
|
+
viewed
|
|
80
|
+
saved
|
|
81
|
+
notInterested
|
|
82
|
+
client { __typename id fullName }
|
|
83
|
+
}
|
|
84
|
+
totalCount
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}`;
|
|
88
|
+
const JOB_SHOW_QUERY = `query JobShow($id: ID!) {
|
|
89
|
+
viewer {
|
|
90
|
+
__typename
|
|
91
|
+
id
|
|
92
|
+
job(id: $id) {
|
|
93
|
+
__typename
|
|
94
|
+
id
|
|
95
|
+
title
|
|
96
|
+
url
|
|
97
|
+
descriptionMd
|
|
98
|
+
commitment { __typename slug }
|
|
99
|
+
workType { __typename slug }
|
|
100
|
+
specialization { __typename title }
|
|
101
|
+
expectedHours
|
|
102
|
+
minimumHoursPerBillingCycle
|
|
103
|
+
maxRate
|
|
104
|
+
startDate
|
|
105
|
+
postedWhen
|
|
106
|
+
viewed
|
|
107
|
+
saved
|
|
108
|
+
notInterested
|
|
109
|
+
isCoaching
|
|
110
|
+
isToptalProject
|
|
111
|
+
semiMonthlyBilling
|
|
112
|
+
positionsCount
|
|
113
|
+
jobTimeZone {
|
|
114
|
+
__typename
|
|
115
|
+
verbose
|
|
116
|
+
hoursOverlap
|
|
117
|
+
workingTimeFrom
|
|
118
|
+
workingTimeTo
|
|
119
|
+
}
|
|
120
|
+
client {
|
|
121
|
+
__typename
|
|
122
|
+
id
|
|
123
|
+
city
|
|
124
|
+
countryName
|
|
125
|
+
fullName
|
|
126
|
+
industry
|
|
127
|
+
isEnterprise
|
|
128
|
+
website
|
|
129
|
+
linkedin
|
|
130
|
+
teamSize { __typename value }
|
|
131
|
+
}
|
|
132
|
+
jobSkillSetsV2 {
|
|
133
|
+
__typename
|
|
134
|
+
edges {
|
|
135
|
+
__typename
|
|
136
|
+
node {
|
|
137
|
+
__typename
|
|
138
|
+
rating
|
|
139
|
+
isOptional
|
|
140
|
+
theSkill { __typename id name }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
languages { __typename id name }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}`;
|
|
148
|
+
const MARK_JOB_SAVED_MUTATION = `mutation JobMarkSaved($jobID: ID!) {
|
|
149
|
+
job(id: $jobID) {
|
|
150
|
+
__typename
|
|
151
|
+
markSaved(input: {}) {
|
|
152
|
+
__typename
|
|
153
|
+
success
|
|
154
|
+
errors { __typename key message code }
|
|
155
|
+
job { __typename id saved notInterested viewed }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}`;
|
|
159
|
+
const MARK_JOB_NOT_INTERESTED_MUTATION = `mutation JobMarkNotInterested($jobID: ID!, $reason: String!) {
|
|
160
|
+
job(id: $jobID) {
|
|
161
|
+
__typename
|
|
162
|
+
markNotInterested(input: { reason: $reason }) {
|
|
163
|
+
__typename
|
|
164
|
+
success
|
|
165
|
+
errors { __typename key message code }
|
|
166
|
+
job { __typename id saved notInterested viewed }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}`;
|
|
170
|
+
const MARK_JOB_VIEWED_MUTATION = `mutation JobMarkViewed($jobID: ID!) {
|
|
171
|
+
job(id: $jobID) {
|
|
172
|
+
__typename
|
|
173
|
+
markViewed(input: {}) {
|
|
174
|
+
__typename
|
|
175
|
+
success
|
|
176
|
+
errors { __typename key message code }
|
|
177
|
+
job { __typename id saved notInterested viewed }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}`;
|
|
181
|
+
const CLEAR_JOB_INTEREST_MUTATION = `mutation JobClearInterest($jobID: ID!) {
|
|
182
|
+
job(id: $jobID) {
|
|
183
|
+
__typename
|
|
184
|
+
clearInterestStatus(input: {}) {
|
|
185
|
+
__typename
|
|
186
|
+
success
|
|
187
|
+
errors { __typename key message code }
|
|
188
|
+
job { __typename id saved notInterested viewed }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}`;
|
|
192
|
+
const JOB_SEARCH_SUBSCRIPTION_QUERY = `query JobSearchSubscriptionShow {
|
|
193
|
+
viewer {
|
|
194
|
+
__typename
|
|
195
|
+
id
|
|
196
|
+
searchSubscription {
|
|
197
|
+
__typename
|
|
198
|
+
skills
|
|
199
|
+
keywords
|
|
200
|
+
excludeSkills
|
|
201
|
+
excludeKeywords
|
|
202
|
+
commitmentsV2
|
|
203
|
+
workTypesV2
|
|
204
|
+
estimatedLengths
|
|
205
|
+
excludeUnspecifiedBudget
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}`;
|
|
209
|
+
const START_JOB_SUBSCRIPTION_MUTATION = `mutation JobSearchSubscriptionStart($skills: [String!], $keywords: [String!], $excludeSkills: [String!], $excludeKeywords: [String!], $excludeUnspecifiedBudget: Boolean, $commitments: [JobCommitmentFilterEnum!], $workTypes: [JobWorkTypeSlug!], $estimatedLengths: [EstimatedLengthFilterEnum!]) {
|
|
210
|
+
searchSubscription {
|
|
211
|
+
__typename
|
|
212
|
+
start(input: {
|
|
213
|
+
skills: $skills
|
|
214
|
+
keywords: $keywords
|
|
215
|
+
excludeSkills: $excludeSkills
|
|
216
|
+
excludeKeywords: $excludeKeywords
|
|
217
|
+
excludeUnspecifiedBudget: $excludeUnspecifiedBudget
|
|
218
|
+
commitmentsV2: $commitments
|
|
219
|
+
workTypesV2: $workTypes
|
|
220
|
+
estimatedLengths: $estimatedLengths
|
|
221
|
+
}) {
|
|
222
|
+
__typename
|
|
223
|
+
success
|
|
224
|
+
errors { __typename key message code }
|
|
225
|
+
viewer {
|
|
226
|
+
__typename
|
|
227
|
+
id
|
|
228
|
+
searchSubscription {
|
|
229
|
+
__typename
|
|
230
|
+
skills
|
|
231
|
+
keywords
|
|
232
|
+
excludeSkills
|
|
233
|
+
excludeKeywords
|
|
234
|
+
commitmentsV2
|
|
235
|
+
workTypesV2
|
|
236
|
+
estimatedLengths
|
|
237
|
+
excludeUnspecifiedBudget
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}`;
|
|
243
|
+
const TERMINATE_JOB_SUBSCRIPTION_MUTATION = `mutation JobSearchSubscriptionTerminate {
|
|
244
|
+
searchSubscription {
|
|
245
|
+
__typename
|
|
246
|
+
terminate(input: {}) {
|
|
247
|
+
__typename
|
|
248
|
+
success
|
|
249
|
+
errors { __typename key message code }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}`;
|
|
253
|
+
// The mobile-gateway returns at least two distinct GraphQL error
|
|
254
|
+
// messages that both mean "no such job from the caller's perspective":
|
|
255
|
+
// - `"Record not found"` for some lookup paths (kept for safety;
|
|
256
|
+
// originally inferred, not yet observed)
|
|
257
|
+
// - `"Invalid ID"` for malformed/unparseable IDs (live-
|
|
258
|
+
// observed during #148 E2E — see #166)
|
|
259
|
+
// A successful response with `viewer.eligibleJob === null` is the
|
|
260
|
+
// third (live-observed) NOT_FOUND signal; that branch is handled
|
|
261
|
+
// inline in `show()` and `markViewed()` and is not regex-driven.
|
|
262
|
+
const NOT_FOUND_MESSAGE_PATTERN = /Record not found|Invalid ID/i;
|
|
263
|
+
/**
|
|
264
|
+
* Thin per-service wrapper around {@link callGatewayShared} (issue
|
|
265
|
+
* #329). Pins the mobile-gateway surface and the {@link JobsError}
|
|
266
|
+
* domain class.
|
|
267
|
+
*/
|
|
268
|
+
async function callGateway(token, operationName, query, variables, schema) {
|
|
269
|
+
return callGatewayShared("mobile-gateway", token, operationName, query, variables, JobsError, {
|
|
270
|
+
schema,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function projectListItem(entity) {
|
|
274
|
+
return {
|
|
275
|
+
id: entity.id,
|
|
276
|
+
title: entity.title,
|
|
277
|
+
url: entity.url,
|
|
278
|
+
client: entity.client,
|
|
279
|
+
commitment: entity.commitment,
|
|
280
|
+
workType: entity.workType,
|
|
281
|
+
specialization: entity.specialization,
|
|
282
|
+
expectedHours: entity.expectedHours,
|
|
283
|
+
maxRate: entity.maxRate,
|
|
284
|
+
startDate: entity.startDate,
|
|
285
|
+
postedWhen: entity.postedWhen,
|
|
286
|
+
viewed: entity.viewed,
|
|
287
|
+
saved: entity.saved,
|
|
288
|
+
notInterested: entity.notInterested,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function projectJobDetail(entity) {
|
|
292
|
+
const skills = [];
|
|
293
|
+
const edges = entity.jobSkillSetsV2?.edges ?? [];
|
|
294
|
+
for (const edge of edges) {
|
|
295
|
+
const node = edge.node;
|
|
296
|
+
const theSkill = node.theSkill;
|
|
297
|
+
if (theSkill !== null) {
|
|
298
|
+
skills.push({
|
|
299
|
+
id: theSkill.id,
|
|
300
|
+
name: theSkill.name,
|
|
301
|
+
rating: node.rating,
|
|
302
|
+
isOptional: node.isOptional,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
...projectListItem(entity),
|
|
308
|
+
descriptionMd: entity.descriptionMd,
|
|
309
|
+
minimumHoursPerBillingCycle: entity.minimumHoursPerBillingCycle,
|
|
310
|
+
isCoaching: entity.isCoaching,
|
|
311
|
+
isToptalProject: entity.isToptalProject,
|
|
312
|
+
semiMonthlyBilling: entity.semiMonthlyBilling,
|
|
313
|
+
positionsCount: entity.positionsCount,
|
|
314
|
+
jobTimeZone: entity.jobTimeZone,
|
|
315
|
+
client: entity.client,
|
|
316
|
+
skills,
|
|
317
|
+
languages: (entity.languages ?? []).map((lang) => ({ id: lang.id, name: lang.name })),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function projectSubscription(entity) {
|
|
321
|
+
if (entity === null) {
|
|
322
|
+
return { active: false, filters: null };
|
|
323
|
+
}
|
|
324
|
+
const filters = {};
|
|
325
|
+
if (entity.skills !== null)
|
|
326
|
+
filters.skills = entity.skills;
|
|
327
|
+
if (entity.keywords !== null)
|
|
328
|
+
filters.keywords = entity.keywords;
|
|
329
|
+
if (entity.excludeSkills !== null)
|
|
330
|
+
filters.excludeSkills = entity.excludeSkills;
|
|
331
|
+
if (entity.excludeKeywords !== null)
|
|
332
|
+
filters.excludeKeywords = entity.excludeKeywords;
|
|
333
|
+
if (entity.commitmentsV2 !== null)
|
|
334
|
+
filters.commitments = entity.commitmentsV2;
|
|
335
|
+
if (entity.workTypesV2 !== null)
|
|
336
|
+
filters.workTypes = entity.workTypesV2;
|
|
337
|
+
if (entity.estimatedLengths !== null)
|
|
338
|
+
filters.estimatedLengths = entity.estimatedLengths;
|
|
339
|
+
if (entity.excludeUnspecifiedBudget !== null)
|
|
340
|
+
filters.excludeUnspecifiedBudget = entity.excludeUnspecifiedBudget;
|
|
341
|
+
return { active: true, filters };
|
|
342
|
+
}
|
|
343
|
+
function formatMutationErrors(operationName, errors) {
|
|
344
|
+
if (errors === null || errors === undefined || errors.length === 0) {
|
|
345
|
+
return `${operationName}: mutation reported failure but returned no errors`;
|
|
346
|
+
}
|
|
347
|
+
return `${operationName}: ${errors
|
|
348
|
+
.map((e) => `${e.key ?? "(no key)"}: ${e.message ?? "(no message)"} (code: ${e.code ?? "unknown"})`)
|
|
349
|
+
.join("; ")}`;
|
|
350
|
+
}
|
|
351
|
+
function buildListVariables(opts, extras) {
|
|
352
|
+
// Pagination defaults: `page: 1, perPage: 20` map to the wire's
|
|
353
|
+
// 1-indexed `page: 1` (= first page). The captured `InitialJobs`
|
|
354
|
+
// operation hardcoded `page: 0` literally, but empirical E2E
|
|
355
|
+
// testing during #138 verification revealed the wire's `page` field
|
|
356
|
+
// is **1-indexed** — `page: 0` and `page: 1` both return the same
|
|
357
|
+
// first slice (the gateway treats 0 as a default-to-first alias).
|
|
358
|
+
// Pages 2, 3, … navigate normally. The pre-#138 hardcoded `page: 0`
|
|
359
|
+
// worked because of this aliasing, NOT because the wire is
|
|
360
|
+
// 0-indexed. The user-facing `--page` is 1-indexed and threads
|
|
361
|
+
// through verbatim; no subtraction.
|
|
362
|
+
const page = opts.page ?? DEFAULT_PAGE;
|
|
363
|
+
const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
|
|
364
|
+
const variables = {
|
|
365
|
+
skills: opts.skills && opts.skills.length > 0 ? opts.skills : null,
|
|
366
|
+
keywords: opts.keywords && opts.keywords.length > 0 ? opts.keywords : null,
|
|
367
|
+
excludeSkills: opts.excludeSkills && opts.excludeSkills.length > 0 ? opts.excludeSkills : null,
|
|
368
|
+
excludeKeywords: opts.excludeKeywords && opts.excludeKeywords.length > 0 ? opts.excludeKeywords : null,
|
|
369
|
+
commitments: opts.commitments && opts.commitments.length > 0 ? opts.commitments : null,
|
|
370
|
+
workTypes: opts.workTypes && opts.workTypes.length > 0 ? opts.workTypes : null,
|
|
371
|
+
estimatedLengths: opts.estimatedLengths && opts.estimatedLengths.length > 0 ? opts.estimatedLengths : null,
|
|
372
|
+
sortTarget: opts.sortTarget ?? null,
|
|
373
|
+
page,
|
|
374
|
+
pageSize: perPage,
|
|
375
|
+
};
|
|
376
|
+
variables["saved"] = extras.saved !== undefined ? { eq: extras.saved } : null;
|
|
377
|
+
variables["notInterested"] = extras.notInterested !== undefined ? { eq: extras.notInterested } : null;
|
|
378
|
+
return variables;
|
|
379
|
+
}
|
|
380
|
+
// ---------------------------------------------------------------------
|
|
381
|
+
// Public surface
|
|
382
|
+
// ---------------------------------------------------------------------
|
|
383
|
+
/**
|
|
384
|
+
* Browse current job opportunities (default sort, paginated).
|
|
385
|
+
*
|
|
386
|
+
* Filters fold straight through to the wire (`eligibleJobs`
|
|
387
|
+
* arguments). Empty arrays / undefined values pass as `null`, letting
|
|
388
|
+
* the server apply its defaults.
|
|
389
|
+
*
|
|
390
|
+
* Pagination (#138): `opts.page` (1-indexed) and `opts.perPage` are
|
|
391
|
+
* forwarded to the wire's 1-indexed `eligibleJobs.page` and
|
|
392
|
+
* `pageSize`. Defaults: `page: 1, perPage: 20`. The wire's `page` is
|
|
393
|
+
* 1-indexed — see {@link buildListVariables} for the empirical
|
|
394
|
+
* findings from #138 E2E verification. Returns a {@link
|
|
395
|
+
* JobListPage} carrying `totalCount` so callers can render
|
|
396
|
+
* offset-style pagination metadata.
|
|
397
|
+
*/
|
|
398
|
+
export async function list(token, opts = {}) {
|
|
399
|
+
const page = opts.page ?? DEFAULT_PAGE;
|
|
400
|
+
const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
|
|
401
|
+
const data = await callGateway(token, "JobsList", JOBS_LIST_QUERY, buildListVariables(opts, {}));
|
|
402
|
+
if (data.viewer === null) {
|
|
403
|
+
throw new JobsError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
404
|
+
}
|
|
405
|
+
if (data.viewer.eligibleJobs === null) {
|
|
406
|
+
return { items: [], totalCount: 0, page, perPage };
|
|
407
|
+
}
|
|
408
|
+
const items = (data.viewer.eligibleJobs.entities ?? []).map(projectListItem);
|
|
409
|
+
return { items, totalCount: data.viewer.eligibleJobs.totalCount, page, perPage };
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Fetch a single job by id. Throws `JobsError("NOT_FOUND")` for two
|
|
413
|
+
* wire shapes: top-level `Record not found` GraphQL error AND a
|
|
414
|
+
* successful response with `viewer.job === null`.
|
|
415
|
+
*/
|
|
416
|
+
export async function show(token, id) {
|
|
417
|
+
let data;
|
|
418
|
+
try {
|
|
419
|
+
data = await callGateway(token, "JobShow", JOB_SHOW_QUERY, { id });
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
if (err instanceof JobsError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
|
|
423
|
+
throw new JobsError("NOT_FOUND", `No job found with id "${id}" (or you don't have access to it).`, {
|
|
424
|
+
cause: err,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
if (data.viewer === null) {
|
|
430
|
+
throw new JobsError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
431
|
+
}
|
|
432
|
+
if (data.viewer.job === null) {
|
|
433
|
+
throw new JobsError("NOT_FOUND", `No job found with id "${id}" (or you don't have access to it).`);
|
|
434
|
+
}
|
|
435
|
+
return projectJobDetail(data.viewer.job);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* List saved jobs (the bookmark / favorites view).
|
|
439
|
+
*
|
|
440
|
+
* Implementation: `eligibleJobs` with `filter: { saved: { eq: true } }`
|
|
441
|
+
* — the same projection as {@link list} so the CLI can reuse the table
|
|
442
|
+
* renderer.
|
|
443
|
+
*
|
|
444
|
+
* Pagination (#138): accepts `opts.page` / `opts.perPage`; returns
|
|
445
|
+
* a {@link JobListPage} for offset-style envelope rendering.
|
|
446
|
+
*/
|
|
447
|
+
export async function saved(token, opts = {}) {
|
|
448
|
+
const page = opts.page ?? DEFAULT_PAGE;
|
|
449
|
+
const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
|
|
450
|
+
const data = await callGateway(token, "JobsList", JOBS_LIST_QUERY, buildListVariables(opts, { saved: true }));
|
|
451
|
+
if (data.viewer === null) {
|
|
452
|
+
throw new JobsError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
453
|
+
}
|
|
454
|
+
if (data.viewer.eligibleJobs === null) {
|
|
455
|
+
return { items: [], totalCount: 0, page, perPage };
|
|
456
|
+
}
|
|
457
|
+
const items = (data.viewer.eligibleJobs.entities ?? []).map(projectListItem);
|
|
458
|
+
return { items, totalCount: data.viewer.eligibleJobs.totalCount, page, perPage };
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* List jobs marked as not-interested. Implementation: `eligibleJobs`
|
|
462
|
+
* with `filter: { notInterested: { eq: true } }`.
|
|
463
|
+
*
|
|
464
|
+
* Pagination (#138): accepts `opts.page` / `opts.perPage`; returns
|
|
465
|
+
* a {@link JobListPage} for offset-style envelope rendering.
|
|
466
|
+
*/
|
|
467
|
+
export async function notInterestedList(token, opts = {}) {
|
|
468
|
+
const page = opts.page ?? DEFAULT_PAGE;
|
|
469
|
+
const perPage = opts.perPage ?? DEFAULT_PER_PAGE;
|
|
470
|
+
const data = await callGateway(token, "JobsList", JOBS_LIST_QUERY, buildListVariables(opts, { notInterested: true }));
|
|
471
|
+
if (data.viewer === null) {
|
|
472
|
+
throw new JobsError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
473
|
+
}
|
|
474
|
+
if (data.viewer.eligibleJobs === null) {
|
|
475
|
+
return { items: [], totalCount: 0, page, perPage };
|
|
476
|
+
}
|
|
477
|
+
const items = (data.viewer.eligibleJobs.entities ?? []).map(projectListItem);
|
|
478
|
+
return { items, totalCount: data.viewer.eligibleJobs.totalCount, page, perPage };
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* List jobs marked as viewed.
|
|
482
|
+
*
|
|
483
|
+
* **R1 — Wire-shape gap**: the `eligibleJobs` query exposes filters
|
|
484
|
+
* only for `saved` and `notInterested`, not `viewed`. This function
|
|
485
|
+
* fetches the requested page of jobs ({@link list}) and applies a
|
|
486
|
+
* client-side filter on the `viewed` boolean. The result is scoped to
|
|
487
|
+
* the items the server returned on that page — pagination shifts the
|
|
488
|
+
* underlying fetch window, but the client-side filter on `viewed`
|
|
489
|
+
* still happens AFTER the page is returned, so the resulting list can
|
|
490
|
+
* be shorter than `perPage`. A wire-level filter would be the right
|
|
491
|
+
* long-term fix and is tracked as a follow-up.
|
|
492
|
+
*
|
|
493
|
+
* Pagination (#138): accepts `opts.page` / `opts.perPage`. The
|
|
494
|
+
* returned {@link JobListPage} carries the `totalCount` of the
|
|
495
|
+
* underlying paginated fetch (pre-filter) — `items.length` reflects
|
|
496
|
+
* the post-filter shape and can be smaller.
|
|
497
|
+
*/
|
|
498
|
+
export async function viewedList(token, opts = {}) {
|
|
499
|
+
const page = await list(token, opts);
|
|
500
|
+
return { ...page, items: page.items.filter((it) => it.viewed === true) };
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Mark a job as saved (bookmark). The wire mutation
|
|
504
|
+
* (`MarkJobAsSaved`) toggles `saved=true` and clears `notInterested=false`
|
|
505
|
+
* if it was set — the server's interest-status model is one-of-three
|
|
506
|
+
* (`saved` | `not-interested` | `cleared`).
|
|
507
|
+
*
|
|
508
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
509
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
510
|
+
* gateway transport and returns it wrapped in {@link
|
|
511
|
+
* JobsDryRunPreviewOutcome}. The bearer token is redacted per the
|
|
512
|
+
* `DryRunPreview` contract; the `jobID` variable carries the caller's
|
|
513
|
+
* literal id (no sibling read needed for this surface).
|
|
514
|
+
*/
|
|
515
|
+
export async function save(token, id, options = {}) {
|
|
516
|
+
const variables = { jobID: id };
|
|
517
|
+
if (options.dryRun === true) {
|
|
518
|
+
return {
|
|
519
|
+
kind: "preview",
|
|
520
|
+
preview: buildDryRunPreview({
|
|
521
|
+
surface: "mobile-gateway",
|
|
522
|
+
authToken: token,
|
|
523
|
+
body: { operationName: "JobMarkSaved", query: MARK_JOB_SAVED_MUTATION, variables },
|
|
524
|
+
}),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const data = await callGateway(token, "JobMarkSaved", MARK_JOB_SAVED_MUTATION, variables);
|
|
528
|
+
return { kind: "applied", result: narrowMutation(data, "markSaved", id, "JobMarkSaved") };
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Clear all interest-status flags on a job. The CLI exposes this as
|
|
532
|
+
* `jobs unsave <id>` (matching the AC) — semantically it also clears
|
|
533
|
+
* `notInterested` because the wire only offers one path
|
|
534
|
+
* (`ClearJobInterestStatus`) to clear EITHER signal. Callers wanting
|
|
535
|
+
* the "remove saved without affecting not-interested" semantics aren't
|
|
536
|
+
* supported by the wire; they would need to re-mark not-interested
|
|
537
|
+
* after.
|
|
538
|
+
*
|
|
539
|
+
* Delegates to {@link clearInterest} (same wire operation
|
|
540
|
+
* `JobClearInterest`) — the dry-run preview therefore reports
|
|
541
|
+
* `operationName: "JobClearInterest"`. The CLI envelope's
|
|
542
|
+
* surface-level `operation` field is `jobs.unsave` (kept distinct so
|
|
543
|
+
* users see the verb they invoked).
|
|
544
|
+
*/
|
|
545
|
+
export async function unsave(token, id, options = {}) {
|
|
546
|
+
return clearInterest(token, id, options);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Mark a job as not-interested with the supplied reason. The wire
|
|
550
|
+
* mutation (`MarkJobAsNotInterested`) toggles `notInterested=true`
|
|
551
|
+
* and clears `saved=false` if it was set.
|
|
552
|
+
*
|
|
553
|
+
* `reason` is server-side `String!` — rejects empty strings with
|
|
554
|
+
* `code=blank, key=reason`. Caller must supply a non-empty value; the
|
|
555
|
+
* wire does not validate against a closed enum, so free-text is fine.
|
|
556
|
+
*
|
|
557
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
558
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
559
|
+
* gateway transport and returns it wrapped in {@link
|
|
560
|
+
* JobsDryRunPreviewOutcome}. The `reason` variable is preserved in the
|
|
561
|
+
* preview's variables payload (it carries no session-bound material) so
|
|
562
|
+
* callers can verify the wire-shape end-to-end.
|
|
563
|
+
*/
|
|
564
|
+
export async function notInterested(token, id, opts, options = {}) {
|
|
565
|
+
const variables = { jobID: id, reason: opts.reason };
|
|
566
|
+
if (options.dryRun === true) {
|
|
567
|
+
return {
|
|
568
|
+
kind: "preview",
|
|
569
|
+
preview: buildDryRunPreview({
|
|
570
|
+
surface: "mobile-gateway",
|
|
571
|
+
authToken: token,
|
|
572
|
+
body: { operationName: "JobMarkNotInterested", query: MARK_JOB_NOT_INTERESTED_MUTATION, variables },
|
|
573
|
+
}),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const data = await callGateway(token, "JobMarkNotInterested", MARK_JOB_NOT_INTERESTED_MUTATION, variables);
|
|
577
|
+
return { kind: "applied", result: narrowMutation(data, "markNotInterested", id, "JobMarkNotInterested") };
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Mark a job as viewed (UX-only signal — typically the UI auto-marks
|
|
581
|
+
* on detail-page open; this surface lets the CLI do it explicitly).
|
|
582
|
+
*
|
|
583
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
584
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
585
|
+
* gateway transport and returns it wrapped in {@link
|
|
586
|
+
* JobsDryRunPreviewOutcome}.
|
|
587
|
+
*/
|
|
588
|
+
export async function markViewed(token, id, options = {}) {
|
|
589
|
+
const variables = { jobID: id };
|
|
590
|
+
if (options.dryRun === true) {
|
|
591
|
+
return {
|
|
592
|
+
kind: "preview",
|
|
593
|
+
preview: buildDryRunPreview({
|
|
594
|
+
surface: "mobile-gateway",
|
|
595
|
+
authToken: token,
|
|
596
|
+
body: { operationName: "JobMarkViewed", query: MARK_JOB_VIEWED_MUTATION, variables },
|
|
597
|
+
}),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const data = await callGateway(token, "JobMarkViewed", MARK_JOB_VIEWED_MUTATION, variables);
|
|
601
|
+
return { kind: "applied", result: narrowMutation(data, "markViewed", id, "JobMarkViewed") };
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Clear the interest-status flags (both `saved` and `notInterested`)
|
|
605
|
+
* on a job. The wire's "undo" path for either save or not-interested.
|
|
606
|
+
*
|
|
607
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
608
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
609
|
+
* gateway transport and returns it wrapped in {@link
|
|
610
|
+
* JobsDryRunPreviewOutcome}.
|
|
611
|
+
*/
|
|
612
|
+
export async function clearInterest(token, id, options = {}) {
|
|
613
|
+
const variables = { jobID: id };
|
|
614
|
+
if (options.dryRun === true) {
|
|
615
|
+
return {
|
|
616
|
+
kind: "preview",
|
|
617
|
+
preview: buildDryRunPreview({
|
|
618
|
+
surface: "mobile-gateway",
|
|
619
|
+
authToken: token,
|
|
620
|
+
body: { operationName: "JobClearInterest", query: CLEAR_JOB_INTEREST_MUTATION, variables },
|
|
621
|
+
}),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const data = await callGateway(token, "JobClearInterest", CLEAR_JOB_INTEREST_MUTATION, variables);
|
|
625
|
+
return { kind: "applied", result: narrowMutation(data, "clearInterestStatus", id, "JobClearInterest") };
|
|
626
|
+
}
|
|
627
|
+
function narrowMutation(data, field, id, operationName) {
|
|
628
|
+
if (data.job === null) {
|
|
629
|
+
throw new JobsError("NOT_FOUND", `No job found with id "${id}" (or you don't have access to it).`);
|
|
630
|
+
}
|
|
631
|
+
const result = data.job[field];
|
|
632
|
+
if (result === null || result === undefined) {
|
|
633
|
+
throw new JobsError("UNKNOWN", `${operationName} returned a null payload for field "${field}".`);
|
|
634
|
+
}
|
|
635
|
+
if (!result.success) {
|
|
636
|
+
throw new JobsError("MUTATION_ERROR", formatMutationErrors(operationName, result.errors));
|
|
637
|
+
}
|
|
638
|
+
if (result.job === null) {
|
|
639
|
+
throw new JobsError("UNKNOWN", `${operationName} returned success but the \`job\` payload was null.`);
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
id: result.job.id,
|
|
643
|
+
saved: result.job.saved,
|
|
644
|
+
notInterested: result.job.notInterested,
|
|
645
|
+
viewed: result.job.viewed,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Show the current job-search subscription state. Returns
|
|
650
|
+
* `{ active: false, filters: null }` when no subscription is active.
|
|
651
|
+
*
|
|
652
|
+
* **R2**: the wire models a single subscription per viewer — there is
|
|
653
|
+
* no list. The CLI's `search list` maps this to a 0-or-1 envelope.
|
|
654
|
+
*/
|
|
655
|
+
export async function searchSubscriptionShow(token) {
|
|
656
|
+
const data = await callGateway(token, "JobSearchSubscriptionShow", JOB_SEARCH_SUBSCRIPTION_QUERY, {});
|
|
657
|
+
if (data.viewer === null) {
|
|
658
|
+
throw new JobsError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
659
|
+
}
|
|
660
|
+
return projectSubscription(data.viewer.searchSubscription);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Start the job-search subscription with the supplied filters. If a
|
|
664
|
+
* subscription is already active, the wire's `start` mutation replaces
|
|
665
|
+
* it (the server does NOT error on "already subscribed").
|
|
666
|
+
*
|
|
667
|
+
* Returns the post-mutation subscription state.
|
|
668
|
+
*
|
|
669
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
670
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
671
|
+
* gateway transport and returns it wrapped in {@link
|
|
672
|
+
* JobsDryRunPreviewOutcome}. The filters payload is normalised
|
|
673
|
+
* identically to the apply path so the preview's `variables` reflect
|
|
674
|
+
* the exact wire shape that WOULD have been sent.
|
|
675
|
+
*/
|
|
676
|
+
export async function searchSubscriptionSave(token, filters, options = {}) {
|
|
677
|
+
const variables = {
|
|
678
|
+
skills: filters.skills && filters.skills.length > 0 ? filters.skills : null,
|
|
679
|
+
keywords: filters.keywords && filters.keywords.length > 0 ? filters.keywords : null,
|
|
680
|
+
excludeSkills: filters.excludeSkills && filters.excludeSkills.length > 0 ? filters.excludeSkills : null,
|
|
681
|
+
excludeKeywords: filters.excludeKeywords && filters.excludeKeywords.length > 0 ? filters.excludeKeywords : null,
|
|
682
|
+
commitments: filters.commitments && filters.commitments.length > 0 ? filters.commitments : null,
|
|
683
|
+
workTypes: filters.workTypes && filters.workTypes.length > 0 ? filters.workTypes : null,
|
|
684
|
+
estimatedLengths: filters.estimatedLengths && filters.estimatedLengths.length > 0 ? filters.estimatedLengths : null,
|
|
685
|
+
excludeUnspecifiedBudget: filters.excludeUnspecifiedBudget ?? null,
|
|
686
|
+
};
|
|
687
|
+
if (options.dryRun === true) {
|
|
688
|
+
return {
|
|
689
|
+
kind: "preview",
|
|
690
|
+
preview: buildDryRunPreview({
|
|
691
|
+
surface: "mobile-gateway",
|
|
692
|
+
authToken: token,
|
|
693
|
+
body: { operationName: "JobSearchSubscriptionStart", query: START_JOB_SUBSCRIPTION_MUTATION, variables },
|
|
694
|
+
}),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const data = await callGateway(token, "JobSearchSubscriptionStart", START_JOB_SUBSCRIPTION_MUTATION, variables);
|
|
698
|
+
if (data.searchSubscription === null) {
|
|
699
|
+
throw new JobsError("UNKNOWN", "JobSearchSubscriptionStart returned a null `searchSubscription` payload.");
|
|
700
|
+
}
|
|
701
|
+
const result = data.searchSubscription.start;
|
|
702
|
+
if (result === null) {
|
|
703
|
+
throw new JobsError("UNKNOWN", "JobSearchSubscriptionStart returned a null `start` payload.");
|
|
704
|
+
}
|
|
705
|
+
if (!result.success) {
|
|
706
|
+
throw new JobsError("MUTATION_ERROR", formatMutationErrors("JobSearchSubscriptionStart", result.errors));
|
|
707
|
+
}
|
|
708
|
+
return { kind: "applied", result: projectSubscription(result.viewer?.searchSubscription ?? null) };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Terminate the active job-search subscription. The wire's `terminate`
|
|
712
|
+
* mutation is idempotent — terminating a non-active subscription
|
|
713
|
+
* returns `success: true` with no errors.
|
|
714
|
+
*
|
|
715
|
+
* Returns `{ terminated: true }` on success. The post-terminate
|
|
716
|
+
* subscription state is implicit (`active: false`) and is not re-
|
|
717
|
+
* fetched here.
|
|
718
|
+
*
|
|
719
|
+
* Dry-run path (issue #162): when invoked with `options.dryRun === true`,
|
|
720
|
+
* builds a {@link DryRunPreview} of the mutation without invoking the
|
|
721
|
+
* gateway transport and returns it wrapped in {@link
|
|
722
|
+
* JobsDryRunPreviewOutcome}. The variables payload is `{}` (the wire
|
|
723
|
+
* `terminate` mutation takes no variables).
|
|
724
|
+
*/
|
|
725
|
+
export async function searchSubscriptionRemove(token, options = {}) {
|
|
726
|
+
if (options.dryRun === true) {
|
|
727
|
+
return {
|
|
728
|
+
kind: "preview",
|
|
729
|
+
preview: buildDryRunPreview({
|
|
730
|
+
surface: "mobile-gateway",
|
|
731
|
+
authToken: token,
|
|
732
|
+
body: {
|
|
733
|
+
operationName: "JobSearchSubscriptionTerminate",
|
|
734
|
+
query: TERMINATE_JOB_SUBSCRIPTION_MUTATION,
|
|
735
|
+
variables: {},
|
|
736
|
+
},
|
|
737
|
+
}),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const data = await callGateway(token, "JobSearchSubscriptionTerminate", TERMINATE_JOB_SUBSCRIPTION_MUTATION, {});
|
|
741
|
+
if (data.searchSubscription === null) {
|
|
742
|
+
throw new JobsError("UNKNOWN", "JobSearchSubscriptionTerminate returned a null `searchSubscription` payload.");
|
|
743
|
+
}
|
|
744
|
+
const result = data.searchSubscription.terminate;
|
|
745
|
+
if (result === null) {
|
|
746
|
+
throw new JobsError("UNKNOWN", "JobSearchSubscriptionTerminate returned a null `terminate` payload.");
|
|
747
|
+
}
|
|
748
|
+
if (!result.success) {
|
|
749
|
+
throw new JobsError("MUTATION_ERROR", formatMutationErrors("JobSearchSubscriptionTerminate", result.errors));
|
|
750
|
+
}
|
|
751
|
+
return { kind: "applied", result: { terminated: true } };
|
|
752
|
+
}
|
|
753
|
+
//# sourceMappingURL=index.js.map
|