@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,324 @@
|
|
|
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 TimesheetError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
name = "TimesheetError";
|
|
8
|
+
constructor(code, message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------
|
|
14
|
+
// Verbatim from `../research/graphql/gateway/operations/mobile/PendingTimesheets.graphql`.
|
|
15
|
+
const PENDING_TIMESHEETS_QUERY = `query PendingTimesheets { viewer { __typename id ...pendingTimesheets } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment pendingTimesheets on Viewer { __typename billingCycles(filters: { pendingTimesheetOnly: true } , pagination: { limit: 50 } ) { __typename nodes { __typename ...timesheetListFields } } }`;
|
|
16
|
+
// Verbatim from `../research/graphql/gateway/operations/mobile/Timesheets.graphql`.
|
|
17
|
+
const TIMESHEETS_QUERY = `query Timesheets($jobActivityItemId: ID!) { viewer { __typename id jobActivityItem(id: $jobActivityItemId) { __typename id engagement { __typename id billingCycles(filters: { onlyTimesheets: true } ) { __typename ids nodes { __typename ...timesheetListFields } } } } } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } }`;
|
|
18
|
+
// Verbatim from `../research/graphql/gateway/operations/mobile/TimesheetDetails.graphql`.
|
|
19
|
+
const TIMESHEET_DETAILS_QUERY = `query TimesheetDetails($id: ID!) { node(id: $id) { __typename ...timesheetDetailsFields } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment contactFieldsData on ContactFields { __typename communitySlackId email phoneNumber skype } fragment timeZoneFields on TimeZone { __typename location value } fragment recruiterData on Recruiter { __typename id fullName contactFields { __typename ...contactFieldsData } photo { __typename small } vacation { __typename id startDate endDate } timeZone { __typename ...timeZoneFields } } fragment pointOfContactData on PointsOfContact { __typename current { __typename ...recruiterData } handoff { __typename ...recruiterData } kind } fragment deliveryModelData on TalentEngagementDeliveryModel { __typename id identifier } fragment timesheetDetailsFields on BillingCycle { __typename ...timesheetListFields timesheetUrl actualAgreement { __typename applicationRate talentHourlyRate marketplaceMargin } engagement { __typename id expectedHours job { __typename id pointsOfContact { __typename ...pointOfContactData } engagementDeliveryModel { __typename ...deliveryModelData } } } timesheetComment timesheetRecords { __typename date duration isDayOff note } }`;
|
|
20
|
+
// Verbatim from `../research/graphql/gateway/operations/mobile/SubmitTimesheet.graphql`.
|
|
21
|
+
const SUBMIT_TIMESHEET_MUTATION = `mutation SubmitTimesheet($id: ID!) { submitTimesheet(billingCycleId: $id) { __typename billingCycle { __typename ...timesheetDetailsFields } ...mutationResultFields } } fragment minimumCommitmentData on MinimumCommitment { __typename applicable minimumHours reasonNotApplicable } fragment timesheetListFields on BillingCycle { __typename id startDate endDate hours minimumCommitment { __typename ...minimumCommitmentData } timesheetOverdue timesheetSubmissionOpenDatetime timesheetSubmissionDeadlineDatetime timesheetSubmitted engagement { __typename id job { __typename id client { __typename id fullName } title } } } fragment contactFieldsData on ContactFields { __typename communitySlackId email phoneNumber skype } fragment timeZoneFields on TimeZone { __typename location value } fragment recruiterData on Recruiter { __typename id fullName contactFields { __typename ...contactFieldsData } photo { __typename small } vacation { __typename id startDate endDate } timeZone { __typename ...timeZoneFields } } fragment pointOfContactData on PointsOfContact { __typename current { __typename ...recruiterData } handoff { __typename ...recruiterData } kind } fragment deliveryModelData on TalentEngagementDeliveryModel { __typename id identifier } fragment timesheetDetailsFields on BillingCycle { __typename ...timesheetListFields timesheetUrl actualAgreement { __typename applicationRate talentHourlyRate marketplaceMargin } engagement { __typename id expectedHours job { __typename id pointsOfContact { __typename ...pointOfContactData } engagementDeliveryModel { __typename ...deliveryModelData } } } timesheetComment timesheetRecords { __typename date duration isDayOff note } } fragment mutationResultFields on MutationResult { __typename errors { __typename key message code } success }`;
|
|
22
|
+
/**
|
|
23
|
+
* Server-side GraphQL error messages that signal "the supplied id does
|
|
24
|
+
* not resolve to a known node" — remapped from `GRAPHQL_ERROR` to the
|
|
25
|
+
* domain-typed `NOT_FOUND` for UX clarity.
|
|
26
|
+
*
|
|
27
|
+
* Empirically observed (E2E 2026-05-12, `TimesheetDetails`, mobile-gateway):
|
|
28
|
+
*
|
|
29
|
+
* "Node id 'VjEtTm9uZXhpc3RlbnQtMA' resolves to an unknown type
|
|
30
|
+
* Nonexistent. Please check if there is no typo and schemas are up
|
|
31
|
+
* to date."
|
|
32
|
+
*
|
|
33
|
+
* This is the Relay-style global-id decode error — Toptal's gateway
|
|
34
|
+
* decodes `<Type>-<localId>` and rejects when the type prefix isn't a
|
|
35
|
+
* known schema node. Match the stable phrase `Node id ... resolves to
|
|
36
|
+
* an unknown type` (typo-tolerant via `.*?`); the historical
|
|
37
|
+
* `Record not found` literal is kept for defense-in-depth against an
|
|
38
|
+
* older message variant.
|
|
39
|
+
*/
|
|
40
|
+
const NOT_FOUND_MESSAGE_PATTERN = /Record not found|Node id .*? resolves to an unknown type/i;
|
|
41
|
+
/**
|
|
42
|
+
* Thin per-service wrapper around {@link callGatewayShared} (issue
|
|
43
|
+
* #329). Pins the mobile-gateway surface and the {@link TimesheetError}
|
|
44
|
+
* domain class.
|
|
45
|
+
*/
|
|
46
|
+
async function callGateway(token, operationName, query, variables, schema) {
|
|
47
|
+
return callGatewayShared("mobile-gateway", token, operationName, query, variables, TimesheetError, { schema });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Project a `TimesheetListWireItem` into the public list shape. Pure
|
|
51
|
+
* shape mirror — present so the list and resolve paths share a single
|
|
52
|
+
* mapper.
|
|
53
|
+
*/
|
|
54
|
+
function projectListItem(wire) {
|
|
55
|
+
return {
|
|
56
|
+
id: wire.id,
|
|
57
|
+
startDate: wire.startDate,
|
|
58
|
+
endDate: wire.endDate,
|
|
59
|
+
hours: wire.hours,
|
|
60
|
+
minimumCommitment: wire.minimumCommitment,
|
|
61
|
+
timesheetOverdue: wire.timesheetOverdue,
|
|
62
|
+
timesheetSubmissionOpenDatetime: wire.timesheetSubmissionOpenDatetime,
|
|
63
|
+
timesheetSubmissionDeadlineDatetime: wire.timesheetSubmissionDeadlineDatetime,
|
|
64
|
+
timesheetSubmitted: wire.timesheetSubmitted,
|
|
65
|
+
engagement: wire.engagement,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function projectDetailItem(wire) {
|
|
69
|
+
return {
|
|
70
|
+
...projectListItem(wire),
|
|
71
|
+
timesheetUrl: wire.timesheetUrl,
|
|
72
|
+
timesheetComment: wire.timesheetComment,
|
|
73
|
+
timesheetRecords: wire.timesheetRecords ?? [],
|
|
74
|
+
actualAgreement: wire.actualAgreement,
|
|
75
|
+
engagement: {
|
|
76
|
+
...wire.engagement,
|
|
77
|
+
expectedHours: wire.engagement.expectedHours,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* List the signed-in user's timesheet billing cycles.
|
|
83
|
+
*
|
|
84
|
+
* Default (no `engagement` option): uses `PendingTimesheets` to fetch
|
|
85
|
+
* viewer-wide cycles that still need submission. This is the
|
|
86
|
+
* "what needs my attention" view.
|
|
87
|
+
*
|
|
88
|
+
* With `engagement` option: uses `Timesheets($jobActivityItemId)` to
|
|
89
|
+
* fetch ALL cycles for that engagement (regardless of submission
|
|
90
|
+
* state). The argument is the public `JobActivityItem.id` exposed by
|
|
91
|
+
* `engagements list`.
|
|
92
|
+
*
|
|
93
|
+
* The returned array preserves server order; the CLI / MCP do not
|
|
94
|
+
* re-sort.
|
|
95
|
+
*/
|
|
96
|
+
export async function list(token, opts = {}) {
|
|
97
|
+
if (opts.engagement === undefined) {
|
|
98
|
+
const data = await callGateway(token, "PendingTimesheets", PENDING_TIMESHEETS_QUERY, {});
|
|
99
|
+
if (data.viewer === null) {
|
|
100
|
+
throw new TimesheetError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
101
|
+
}
|
|
102
|
+
return (data.viewer.billingCycles?.nodes ?? []).map(projectListItem);
|
|
103
|
+
}
|
|
104
|
+
let data;
|
|
105
|
+
try {
|
|
106
|
+
data = await callGateway(token, "Timesheets", TIMESHEETS_QUERY, {
|
|
107
|
+
jobActivityItemId: opts.engagement,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
|
|
112
|
+
throw new TimesheetError("NOT_FOUND", `No engagement found with id "${opts.engagement}" (or you don't have access to it).`, { cause: err });
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
if (data.viewer === null) {
|
|
117
|
+
throw new TimesheetError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
|
|
118
|
+
}
|
|
119
|
+
if (data.viewer.jobActivityItem === null) {
|
|
120
|
+
throw new TimesheetError("NOT_FOUND", `No engagement found with id "${opts.engagement}" (or you don't have access to it).`);
|
|
121
|
+
}
|
|
122
|
+
if (data.viewer.jobActivityItem.engagement === null) {
|
|
123
|
+
throw new TimesheetError("NO_ENGAGEMENT", `Activity item "${opts.engagement}" exists but has no engagement (likely an application or interview).`);
|
|
124
|
+
}
|
|
125
|
+
return (data.viewer.jobActivityItem.engagement.billingCycles?.nodes ?? []).map(projectListItem);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Fetch a single timesheet's detail by `BillingCycle.id`.
|
|
129
|
+
*
|
|
130
|
+
* Uses the gateway's `node(id)` polymorphic root with the captured
|
|
131
|
+
* `timesheetDetailsFields` fragment. Throws
|
|
132
|
+
* `TimesheetError("NOT_FOUND")` when the id doesn't resolve (matches
|
|
133
|
+
* both the `Record not found` GraphQL error path AND the data-shape
|
|
134
|
+
* sentinel `data.node === null`).
|
|
135
|
+
*
|
|
136
|
+
* Throws `TimesheetError("UNKNOWN")` if the node resolves but isn't a
|
|
137
|
+
* `BillingCycle` — that would be a wire surprise (a different `node`
|
|
138
|
+
* type carrying our fragment) and warrants surfacing rather than
|
|
139
|
+
* silent type-coercion.
|
|
140
|
+
*/
|
|
141
|
+
export async function show(token, id) {
|
|
142
|
+
let data;
|
|
143
|
+
try {
|
|
144
|
+
data = await callGateway(token, "TimesheetDetails", TIMESHEET_DETAILS_QUERY, { id });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
|
|
148
|
+
throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to it).`, {
|
|
149
|
+
cause: err,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
if (data.node === null) {
|
|
155
|
+
throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to it).`);
|
|
156
|
+
}
|
|
157
|
+
return projectDetailItem(data.node);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the "current" pending timesheet — the billing cycle whose
|
|
161
|
+
* submission window contains `now` AND which is not yet submitted.
|
|
162
|
+
*
|
|
163
|
+
* - `kind: "found"`: exactly one cycle matches.
|
|
164
|
+
* - `kind: "none"`: zero cycles match (too early before next window,
|
|
165
|
+
* too late after every cycle's deadline, or no engagements have
|
|
166
|
+
* timesheets enabled).
|
|
167
|
+
* - `kind: "multiple"`: more than one cycle matches (parallel
|
|
168
|
+
* engagements with overlapping current windows).
|
|
169
|
+
*
|
|
170
|
+
* When `opts.engagement` is provided, the resolution scopes to that
|
|
171
|
+
* engagement (uses `Timesheets($jobActivityItemId)` + client-side
|
|
172
|
+
* filter to `timesheetSubmitted === false`). Otherwise uses
|
|
173
|
+
* `PendingTimesheets` (server-side filtered).
|
|
174
|
+
*
|
|
175
|
+
* The `now` option exists for deterministic testing; production code
|
|
176
|
+
* paths pass nothing and the helper uses `new Date()`.
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveCurrentCycle(token, opts = {}) {
|
|
179
|
+
const now = opts.now ?? new Date();
|
|
180
|
+
const candidates = await listPending(token, opts.engagement);
|
|
181
|
+
const inWindow = candidates.filter((c) => isInSubmissionWindow(c, now));
|
|
182
|
+
if (inWindow.length === 0)
|
|
183
|
+
return { kind: "none" };
|
|
184
|
+
if (inWindow.length === 1) {
|
|
185
|
+
const cycle = inWindow[0];
|
|
186
|
+
if (cycle === undefined)
|
|
187
|
+
return { kind: "none" };
|
|
188
|
+
return { kind: "found", cycle };
|
|
189
|
+
}
|
|
190
|
+
return { kind: "multiple", candidates: inWindow };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Internal: return the pending (`timesheetSubmitted === false`) cycles
|
|
194
|
+
* — viewer-wide if no engagement scope, scoped + client-filtered if
|
|
195
|
+
* scoped. Shared between {@link resolveCurrentCycle} and any future
|
|
196
|
+
* callers wanting the same pre-filtered list.
|
|
197
|
+
*/
|
|
198
|
+
async function listPending(token, engagement) {
|
|
199
|
+
if (engagement === undefined) {
|
|
200
|
+
return list(token);
|
|
201
|
+
}
|
|
202
|
+
const all = await list(token, { engagement });
|
|
203
|
+
return all.filter((c) => !c.timesheetSubmitted);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Returns `true` when `now` falls within the cycle's submission window
|
|
207
|
+
* AND the cycle is not yet submitted.
|
|
208
|
+
*
|
|
209
|
+
* Submission window is `[timesheetSubmissionOpenDatetime,
|
|
210
|
+
* timesheetSubmissionDeadlineDatetime]` (inclusive). Cycles missing
|
|
211
|
+
* either bound are excluded — defensive: if the server hasn't decided
|
|
212
|
+
* when the window opens/closes, "now" is unambiguously not inside.
|
|
213
|
+
*/
|
|
214
|
+
function isInSubmissionWindow(cycle, now) {
|
|
215
|
+
if (cycle.timesheetSubmitted)
|
|
216
|
+
return false;
|
|
217
|
+
if (cycle.timesheetSubmissionOpenDatetime === null)
|
|
218
|
+
return false;
|
|
219
|
+
if (cycle.timesheetSubmissionDeadlineDatetime === null)
|
|
220
|
+
return false;
|
|
221
|
+
const open = Date.parse(cycle.timesheetSubmissionOpenDatetime);
|
|
222
|
+
const deadline = Date.parse(cycle.timesheetSubmissionDeadlineDatetime);
|
|
223
|
+
if (Number.isNaN(open) || Number.isNaN(deadline))
|
|
224
|
+
return false;
|
|
225
|
+
const t = now.getTime();
|
|
226
|
+
return t >= open && t <= deadline;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Submit a timesheet for billing.
|
|
230
|
+
*
|
|
231
|
+
* **Destructive**: the submission is one-way at the wire level — once
|
|
232
|
+
* submitted, the timesheet enters Toptal's billing pipeline. Callers
|
|
233
|
+
* (CLI / MCP) are responsible for end-user confirmation.
|
|
234
|
+
*
|
|
235
|
+
* `id` is the BillingCycle.id from `list()` / `show()`. Returns the
|
|
236
|
+
* post-submission detail payload (with `timesheetSubmitted: true`)
|
|
237
|
+
* wrapped in `{ kind: "applied", result }` on the apply path.
|
|
238
|
+
*
|
|
239
|
+
* Dry-run path (`options.dryRun === true`): builds a
|
|
240
|
+
* {@link DryRunPreview} of the mutation WITHOUT invoking the
|
|
241
|
+
* gateway transport. Returns `{ kind: "preview", preview }`. The
|
|
242
|
+
* CLI's `--dry-run` flag flows through here so the destructive
|
|
243
|
+
* mutation is never sent in preview mode. See {@link DryRunOptions}
|
|
244
|
+
* for the placeholder-id semantics when the CLI is in auto-resolve
|
|
245
|
+
* mode.
|
|
246
|
+
*
|
|
247
|
+
* Throws (apply path only — dry-run never throws domain errors):
|
|
248
|
+
* - `TimesheetError("NOT_FOUND")` when the id doesn't resolve to a
|
|
249
|
+
* billing cycle the viewer can submit AND the server is willing to
|
|
250
|
+
* communicate that as a structured error (Relay-style global-id
|
|
251
|
+
* decode failure, matched via {@link NOT_FOUND_MESSAGE_PATTERN}).
|
|
252
|
+
* - `TimesheetError("GRAPHQL_ERROR")` when the server returns a
|
|
253
|
+
* top-level GraphQL error other than the Relay decode pattern.
|
|
254
|
+
* Empirically (E2E 2026-05-12), Toptal's `SubmitTimesheet` returns
|
|
255
|
+
* `"500: Internal Server Error"` for syntactically valid but
|
|
256
|
+
* non-existent BillingCycle ids — the wire does not pre-validate the
|
|
257
|
+
* id, so the 500 surfaces verbatim. The CLI presents it as
|
|
258
|
+
* `GRAPHQL_ERROR` (not `NOT_FOUND`) to avoid misleading the caller
|
|
259
|
+
* into thinking the id was definitively absent vs. the server
|
|
260
|
+
* genuinely failed.
|
|
261
|
+
* - `TimesheetError("MUTATION_ERROR")` when the server reports
|
|
262
|
+
* `success: false` on `MutationResult` (commonly: missing required
|
|
263
|
+
* hours, already submitted, deadline passed). The message carries
|
|
264
|
+
* the server-side error code+key+message tuples.
|
|
265
|
+
*/
|
|
266
|
+
export async function submit(token, id, options = {}) {
|
|
267
|
+
if (options.dryRun === true) {
|
|
268
|
+
return {
|
|
269
|
+
kind: "preview",
|
|
270
|
+
preview: buildDryRunPreview({
|
|
271
|
+
surface: "mobile-gateway",
|
|
272
|
+
authToken: token,
|
|
273
|
+
body: {
|
|
274
|
+
operationName: "SubmitTimesheet",
|
|
275
|
+
query: SUBMIT_TIMESHEET_MUTATION,
|
|
276
|
+
variables: { id },
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
let data;
|
|
282
|
+
try {
|
|
283
|
+
data = await callGateway(token, "SubmitTimesheet", SUBMIT_TIMESHEET_MUTATION, { id });
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
// Remap Relay-style id-decode failures from GRAPHQL_ERROR to NOT_FOUND
|
|
287
|
+
// for consistent UX with `show()` and the engagement-scoped `list()`.
|
|
288
|
+
// The 500-on-bad-id case (no decode error, just a server crash) flows
|
|
289
|
+
// through verbatim as GRAPHQL_ERROR — see function docstring.
|
|
290
|
+
if (err instanceof TimesheetError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
|
|
291
|
+
throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to submit it).`, {
|
|
292
|
+
cause: err,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
const payload = data.submitTimesheet;
|
|
298
|
+
if (payload === null) {
|
|
299
|
+
throw new TimesheetError("NOT_FOUND", `No timesheet found with id "${id}" (or you don't have access to submit it).`);
|
|
300
|
+
}
|
|
301
|
+
if (!payload.success) {
|
|
302
|
+
throw new TimesheetError("MUTATION_ERROR", formatMutationErrors("SubmitTimesheet rejected", payload.errors));
|
|
303
|
+
}
|
|
304
|
+
if (payload.billingCycle === null) {
|
|
305
|
+
throw new TimesheetError("UNKNOWN", "SubmitTimesheet succeeded but the server returned no updated billingCycle payload.");
|
|
306
|
+
}
|
|
307
|
+
return { kind: "applied", result: projectDetailItem(payload.billingCycle) };
|
|
308
|
+
}
|
|
309
|
+
function formatMutationErrors(prefix, errors) {
|
|
310
|
+
if (errors == null || errors.length === 0) {
|
|
311
|
+
return `${prefix}: no error detail returned.`;
|
|
312
|
+
}
|
|
313
|
+
const parts = errors.map((e) => {
|
|
314
|
+
const fields = [];
|
|
315
|
+
if (e.code != null)
|
|
316
|
+
fields.push(`code=${e.code}`);
|
|
317
|
+
if (e.key != null)
|
|
318
|
+
fields.push(`key=${e.key}`);
|
|
319
|
+
const head = fields.length > 0 ? `[${fields.join(", ")}] ` : "";
|
|
320
|
+
return `${head}${e.message ?? "(no message)"}`;
|
|
321
|
+
});
|
|
322
|
+
return `${prefix}: ${parts.join("; ")}`;
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/services/timesheet/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-field ↔ CLI-flag translation tables. Centralizes the mapping
|
|
3
|
+
* between Toptal GraphQL field names (server-side, non-canonical) and the
|
|
4
|
+
* canonical CLI flag names that ttctl exposes to users.
|
|
5
|
+
*
|
|
6
|
+
* Naming policy (see issue #72): the canonical vocabulary is Toptal-faithful
|
|
7
|
+
* with the user-friendly CLI surface as the public API. When a Toptal field
|
|
8
|
+
* name reads poorly as a flag (`quote` for what users would call a
|
|
9
|
+
* "headline", `about` for what users would call a "bio"), this table
|
|
10
|
+
* translates between the two — letting `core` services speak the GraphQL
|
|
11
|
+
* vocabulary while the CLI / MCP surfaces speak the user-friendly one.
|
|
12
|
+
*
|
|
13
|
+
* Conventions:
|
|
14
|
+
*
|
|
15
|
+
* - Each table is a flat `Record<string, string>` from server field name
|
|
16
|
+
* (key) to CLI flag name (value).
|
|
17
|
+
* - Tables are `as const` so consumers can derive precise key/value union
|
|
18
|
+
* types when needed.
|
|
19
|
+
* - One table per sub-domain (basic, skills, …). Sub-domains beyond `basic`
|
|
20
|
+
* land in their respective issues (#70/#71/#73-#76); this file gets
|
|
21
|
+
* amended, not split, as those land.
|
|
22
|
+
*
|
|
23
|
+
* The CLI / MCP layer is the single consumer of these tables. Service code
|
|
24
|
+
* inside `core` continues to use the GraphQL field names directly — the
|
|
25
|
+
* boundary at which translation happens is the `presentation` boundary
|
|
26
|
+
* (CLI flag parsing, MCP tool input shaping), NOT the service boundary.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Server-field → CLI-flag mapping for the `basic` profile sub-domain.
|
|
30
|
+
*
|
|
31
|
+
* Covers every `basic` field whose user-facing CLI flag differs from its
|
|
32
|
+
* GraphQL name. Fields whose CLI flag matches their GraphQL name (e.g.,
|
|
33
|
+
* `email`, `fullName`) are NOT listed — `serverToCli` / `cliToServer`
|
|
34
|
+
* pass unmapped keys through unchanged. Each entry encodes a deliberate
|
|
35
|
+
* vocabulary choice — see the per-line rationale.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PROFILE_BASIC_FIELDS: {
|
|
38
|
+
readonly quote: "headline";
|
|
39
|
+
readonly about: "bio";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Server-field → CLI-flag mapping for the `skills` profile sub-domain.
|
|
43
|
+
*
|
|
44
|
+
* Currently empty: every field on `ProfileSkillSet` (`experience`, `rating`,
|
|
45
|
+
* `public`, `position`, `skill.{id,name}`) reads naturally as a CLI flag
|
|
46
|
+
* with no rename. The empty table is registered for two reasons:
|
|
47
|
+
*
|
|
48
|
+
* 1. **Structural symmetry** with `PROFILE_BASIC_FIELDS` — consumers can
|
|
49
|
+
* mechanically derive `serverToCli`/`cliToServer` calls per sub-domain
|
|
50
|
+
* regardless of whether any field actually renames, so future entries
|
|
51
|
+
* can be added in-place without changing call-sites.
|
|
52
|
+
* 2. **Discovery surface** — when a future field needs renaming
|
|
53
|
+
* (e.g., a server-side `experience` that's actually expressed in months
|
|
54
|
+
* while the CLI exposes `--experience-years`), this is the documented
|
|
55
|
+
* home for that mapping.
|
|
56
|
+
*
|
|
57
|
+
* The empty `as const` literal still produces a `Readonly<Record<string,
|
|
58
|
+
* string>>` per the `satisfies` clause, so `serverToCli(_, PROFILE_SKILL_FIELDS)`
|
|
59
|
+
* type-checks without the consumer having to special-case the empty case.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PROFILE_SKILL_FIELDS: {};
|
|
62
|
+
/**
|
|
63
|
+
* Translate a server-keyed object (GraphQL response shape) to a CLI-keyed
|
|
64
|
+
* object (user-facing flag shape). Keys present in the table are renamed;
|
|
65
|
+
* keys absent from the table pass through unchanged. Values are copied
|
|
66
|
+
* verbatim — translation is a key-rename pass, not a value transform.
|
|
67
|
+
*
|
|
68
|
+
* Returns a fresh object; the input is not mutated.
|
|
69
|
+
*/
|
|
70
|
+
export declare function serverToCli<V>(serverObj: Readonly<Record<string, V>>, table: Readonly<Record<string, string>>): Record<string, V>;
|
|
71
|
+
/**
|
|
72
|
+
* Translate a CLI-keyed object (user-facing flag shape) to a server-keyed
|
|
73
|
+
* object (GraphQL input shape). Symmetric counterpart of `serverToCli`.
|
|
74
|
+
* Keys absent from the inverse mapping pass through unchanged.
|
|
75
|
+
*
|
|
76
|
+
* Returns a fresh object; the input is not mutated.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cliToServer<V>(cliObj: Readonly<Record<string, V>>, table: Readonly<Record<string, string>>): Record<string, V>;
|
|
79
|
+
//# sourceMappingURL=translations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../src/services/translations.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;;;CAQoB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oBAAoB,IAEoB,CAAC;AAuCtD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAOnB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAQnB"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 Oleksii PELYKH
|
|
3
|
+
/**
|
|
4
|
+
* Server-field ↔ CLI-flag translation tables. Centralizes the mapping
|
|
5
|
+
* between Toptal GraphQL field names (server-side, non-canonical) and the
|
|
6
|
+
* canonical CLI flag names that ttctl exposes to users.
|
|
7
|
+
*
|
|
8
|
+
* Naming policy (see issue #72): the canonical vocabulary is Toptal-faithful
|
|
9
|
+
* with the user-friendly CLI surface as the public API. When a Toptal field
|
|
10
|
+
* name reads poorly as a flag (`quote` for what users would call a
|
|
11
|
+
* "headline", `about` for what users would call a "bio"), this table
|
|
12
|
+
* translates between the two — letting `core` services speak the GraphQL
|
|
13
|
+
* vocabulary while the CLI / MCP surfaces speak the user-friendly one.
|
|
14
|
+
*
|
|
15
|
+
* Conventions:
|
|
16
|
+
*
|
|
17
|
+
* - Each table is a flat `Record<string, string>` from server field name
|
|
18
|
+
* (key) to CLI flag name (value).
|
|
19
|
+
* - Tables are `as const` so consumers can derive precise key/value union
|
|
20
|
+
* types when needed.
|
|
21
|
+
* - One table per sub-domain (basic, skills, …). Sub-domains beyond `basic`
|
|
22
|
+
* land in their respective issues (#70/#71/#73-#76); this file gets
|
|
23
|
+
* amended, not split, as those land.
|
|
24
|
+
*
|
|
25
|
+
* The CLI / MCP layer is the single consumer of these tables. Service code
|
|
26
|
+
* inside `core` continues to use the GraphQL field names directly — the
|
|
27
|
+
* boundary at which translation happens is the `presentation` boundary
|
|
28
|
+
* (CLI flag parsing, MCP tool input shaping), NOT the service boundary.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Server-field → CLI-flag mapping for the `basic` profile sub-domain.
|
|
32
|
+
*
|
|
33
|
+
* Covers every `basic` field whose user-facing CLI flag differs from its
|
|
34
|
+
* GraphQL name. Fields whose CLI flag matches their GraphQL name (e.g.,
|
|
35
|
+
* `email`, `fullName`) are NOT listed — `serverToCli` / `cliToServer`
|
|
36
|
+
* pass unmapped keys through unchanged. Each entry encodes a deliberate
|
|
37
|
+
* vocabulary choice — see the per-line rationale.
|
|
38
|
+
*/
|
|
39
|
+
export const PROFILE_BASIC_FIELDS = {
|
|
40
|
+
// GraphQL `Profile.quote` is the short tagline shown above the user's
|
|
41
|
+
// photo. "Quote" reads poorly as a CLI flag (`--quote` suggests pricing or
|
|
42
|
+
// an exact citation); the user-facing name is `headline`.
|
|
43
|
+
quote: "headline",
|
|
44
|
+
// GraphQL `Profile.about` is the long-form bio paragraph. The CLI flag
|
|
45
|
+
// calls this `bio` (shorter and idiomatic).
|
|
46
|
+
about: "bio",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Server-field → CLI-flag mapping for the `skills` profile sub-domain.
|
|
50
|
+
*
|
|
51
|
+
* Currently empty: every field on `ProfileSkillSet` (`experience`, `rating`,
|
|
52
|
+
* `public`, `position`, `skill.{id,name}`) reads naturally as a CLI flag
|
|
53
|
+
* with no rename. The empty table is registered for two reasons:
|
|
54
|
+
*
|
|
55
|
+
* 1. **Structural symmetry** with `PROFILE_BASIC_FIELDS` — consumers can
|
|
56
|
+
* mechanically derive `serverToCli`/`cliToServer` calls per sub-domain
|
|
57
|
+
* regardless of whether any field actually renames, so future entries
|
|
58
|
+
* can be added in-place without changing call-sites.
|
|
59
|
+
* 2. **Discovery surface** — when a future field needs renaming
|
|
60
|
+
* (e.g., a server-side `experience` that's actually expressed in months
|
|
61
|
+
* while the CLI exposes `--experience-years`), this is the documented
|
|
62
|
+
* home for that mapping.
|
|
63
|
+
*
|
|
64
|
+
* The empty `as const` literal still produces a `Readonly<Record<string,
|
|
65
|
+
* string>>` per the `satisfies` clause, so `serverToCli(_, PROFILE_SKILL_FIELDS)`
|
|
66
|
+
* type-checks without the consumer having to special-case the empty case.
|
|
67
|
+
*/
|
|
68
|
+
export const PROFILE_SKILL_FIELDS = {
|
|
69
|
+
// Currently empty — see module docstring above for rationale.
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Reverse the direction of a translation table — CLI-flag → server-field.
|
|
73
|
+
*
|
|
74
|
+
* Used by `cliToServer` to translate user-supplied option objects into the
|
|
75
|
+
* shape expected by GraphQL inputs. Computed once per import via the
|
|
76
|
+
* `INVERSE_*` constants below; do NOT call this at request time (it
|
|
77
|
+
* allocates a fresh object).
|
|
78
|
+
*/
|
|
79
|
+
function invertTable(table) {
|
|
80
|
+
const inverse = {};
|
|
81
|
+
for (const [server, cli] of Object.entries(table)) {
|
|
82
|
+
inverse[cli] = server;
|
|
83
|
+
}
|
|
84
|
+
return inverse;
|
|
85
|
+
}
|
|
86
|
+
const INVERSE_PROFILE_BASIC_FIELDS = invertTable(PROFILE_BASIC_FIELDS);
|
|
87
|
+
const INVERSE_PROFILE_SKILL_FIELDS = invertTable(PROFILE_SKILL_FIELDS);
|
|
88
|
+
/**
|
|
89
|
+
* Pre-computed inverse tables (CLI-flag → server-field) keyed by their
|
|
90
|
+
* forward counterpart, so callers can pass either direction's table to a
|
|
91
|
+
* shared helper. Kept private — `cliToServer` resolves the right inverse
|
|
92
|
+
* via `getInverse()` rather than asking callers to pass it.
|
|
93
|
+
*/
|
|
94
|
+
const INVERSES = new WeakMap([
|
|
95
|
+
[PROFILE_BASIC_FIELDS, INVERSE_PROFILE_BASIC_FIELDS],
|
|
96
|
+
[PROFILE_SKILL_FIELDS, INVERSE_PROFILE_SKILL_FIELDS],
|
|
97
|
+
]);
|
|
98
|
+
function getInverse(table) {
|
|
99
|
+
// Tables registered above hit the WeakMap; ad-hoc tables (e.g., from a
|
|
100
|
+
// unit test) compute their inverse on the fly. The fast path is the
|
|
101
|
+
// expected one — service code always passes a registered table.
|
|
102
|
+
return INVERSES.get(table) ?? invertTable(table);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Translate a server-keyed object (GraphQL response shape) to a CLI-keyed
|
|
106
|
+
* object (user-facing flag shape). Keys present in the table are renamed;
|
|
107
|
+
* keys absent from the table pass through unchanged. Values are copied
|
|
108
|
+
* verbatim — translation is a key-rename pass, not a value transform.
|
|
109
|
+
*
|
|
110
|
+
* Returns a fresh object; the input is not mutated.
|
|
111
|
+
*/
|
|
112
|
+
export function serverToCli(serverObj, table) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const [key, value] of Object.entries(serverObj)) {
|
|
115
|
+
const cliKey = table[key] ?? key;
|
|
116
|
+
out[cliKey] = value;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Translate a CLI-keyed object (user-facing flag shape) to a server-keyed
|
|
122
|
+
* object (GraphQL input shape). Symmetric counterpart of `serverToCli`.
|
|
123
|
+
* Keys absent from the inverse mapping pass through unchanged.
|
|
124
|
+
*
|
|
125
|
+
* Returns a fresh object; the input is not mutated.
|
|
126
|
+
*/
|
|
127
|
+
export function cliToServer(cliObj, table) {
|
|
128
|
+
const inverse = getInverse(table);
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const [key, value] of Object.entries(cliObj)) {
|
|
131
|
+
const serverKey = inverse[key] ?? key;
|
|
132
|
+
out[serverKey] = value;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=translations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translations.js","sourceRoot":"","sources":["../../src/services/translations.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,oCAAoC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,sEAAsE;IACtE,2EAA2E;IAC3E,0DAA0D;IAC1D,KAAK,EAAE,UAAU;IACjB,uEAAuE;IACvE,4CAA4C;IAC5C,KAAK,EAAE,KAAK;CACuC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;AAClC,8DAA8D;CACX,CAAC;AAEtD;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,KAAuC;IAC1D,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;IACxB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,4BAA4B,GAAG,WAAW,CAAC,oBAAoB,CAAC,CAAC;AACvE,MAAM,4BAA4B,GAAG,WAAW,CAAC,oBAAoB,CAAC,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAqE;IAC/F,CAAC,oBAAoB,EAAE,4BAA4B,CAAC;IACpD,CAAC,oBAAoB,EAAE,4BAA4B,CAAC;CACrD,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,KAAuC;IACzD,uEAAuE;IACvE,oEAAoE;IACpE,gEAAgE;IAChE,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CACzB,SAAsC,EACtC,KAAuC;IAEvC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QACjC,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,MAAmC,EACnC,KAAuC;IAEvC,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QACtC,GAAG,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;IACzB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|