node-autotask 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/MCP_TOOL_CATALOG.json +156 -1253
- package/MCP_TOOL_MANIFEST.md +154 -220
- package/MCP_TOOL_OVERRIDES.json +8 -62
- package/N8N_MCP_MANIFEST.json +128 -705
- package/N8N_MCP_MANIFEST.md +43 -63
- package/README.md +10 -28
- package/capabilities.json +15315 -22219
- package/dist/{base-CVx2VMSj.d.cts → base-BVoYDlVA.d.cts} +2 -1
- package/dist/{base-CVx2VMSj.d.ts → base-BVoYDlVA.d.ts} +2 -1
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.d.cts +2 -1
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-5VHSLJMR.js → chunk-A6LHCJQP.js} +30 -9
- package/dist/{chunk-JY2BWYXY.js → chunk-ASMCTZO2.js} +15314 -22218
- package/dist/{chunk-TH7B5H4P.cjs → chunk-DC3PY4EF.cjs} +1839 -1407
- package/dist/{chunk-4EFXIE53.js → chunk-DXKXQHCX.js} +5 -5
- package/dist/{chunk-SK3GYRNA.cjs → chunk-JO6WI763.cjs} +143 -143
- package/dist/{chunk-Q23JULNN.js → chunk-NSW7JPVS.js} +11529 -13319
- package/dist/{chunk-KTEDPS4X.js → chunk-O4PIPUS4.js} +1583 -1151
- package/dist/{chunk-MJTKK3QG.cjs → chunk-SN4BMQ2N.cjs} +31 -10
- package/dist/{chunk-5BDUMDAQ.cjs → chunk-VXVYFJH3.cjs} +15314 -22218
- package/dist/{chunk-VFLAMMIJ.cjs → chunk-YMBVKVK4.cjs} +10604 -12394
- package/dist/{index-C7rMJ4IV.d.ts → index-C5g1H_hF.d.ts} +1080 -1288
- package/dist/{index-Cm_Xpt7X.d.cts → index-Db0SNnQn.d.cts} +1080 -1288
- package/dist/{index-C3KFMnRT.d.cts → index-c549LvOb.d.cts} +332 -331
- package/dist/{index-C3KFMnRT.d.ts → index-c549LvOb.d.ts} +332 -331
- package/dist/index.cjs +23 -23
- package/dist/index.d.cts +15 -41
- package/dist/index.d.ts +15 -41
- package/dist/index.js +8 -8
- package/dist/operations/index.cjs +5 -2234
- package/dist/operations/index.d.cts +51 -323
- package/dist/operations/index.d.ts +51 -323
- package/dist/operations/index.js +30 -2259
- package/dist/pool.cjs +9 -9
- package/dist/pool.d.cts +4 -3
- package/dist/pool.d.ts +4 -3
- package/dist/pool.js +5 -5
- package/dist/resources/index.cjs +5 -5
- package/dist/resources/index.d.cts +2 -2
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.js +8 -8
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/untrusted.cjs +2 -2
- package/dist/untrusted.d.cts +14 -5
- package/dist/untrusted.d.ts +14 -5
- package/dist/untrusted.js +1 -1
- package/docs/API.md +321 -334
- package/docs/LIVE-TESTING.md +4 -6
- package/docs/N8N-ADAPTER.md +26 -26
- package/examples/mcp/schemas.ts +2 -0
- package/examples/mcp/tools-read.ts +3 -2
- package/n8n-mcp.curation.json +3 -3
- package/package.json +10 -9
|
@@ -3,37 +3,47 @@ import {
|
|
|
3
3
|
AutotaskError,
|
|
4
4
|
BaseResource,
|
|
5
5
|
ConflictError,
|
|
6
|
+
FETCH_BY_IDS_CHUNK_SIZE,
|
|
7
|
+
IN_FILTER_CAP,
|
|
8
|
+
MoveRefusedError,
|
|
6
9
|
NotFoundError,
|
|
7
10
|
RELATIONSHIP_MAP,
|
|
11
|
+
ResolutionError,
|
|
12
|
+
ResponseRejectedError,
|
|
8
13
|
StaleObjectError,
|
|
9
14
|
TICKET_LABEL_FIELDS,
|
|
10
15
|
addLabelSibling,
|
|
11
16
|
addNameSibling,
|
|
17
|
+
budgetExceededError,
|
|
12
18
|
buildDryRunResult,
|
|
19
|
+
buildUiLink,
|
|
20
|
+
clientOf,
|
|
21
|
+
compositePart,
|
|
22
|
+
compositePartLimit,
|
|
13
23
|
createNamePlan,
|
|
14
24
|
cursorToPath,
|
|
15
|
-
|
|
25
|
+
invokeRead,
|
|
16
26
|
isAutotaskError,
|
|
17
27
|
isClosedStatus,
|
|
28
|
+
makeCallBudget,
|
|
18
29
|
mergePartialBody,
|
|
30
|
+
numericIds,
|
|
19
31
|
openStatusFilter,
|
|
32
|
+
openTicketsOfPart,
|
|
20
33
|
parseIdentifier,
|
|
21
34
|
parseVendorErrors,
|
|
22
|
-
queryRows,
|
|
23
|
-
refuse,
|
|
24
35
|
resolveCompositeNames,
|
|
25
36
|
resolveLineOfBusinessChain,
|
|
26
37
|
resolveTicketStatusSemantics,
|
|
27
|
-
runMove,
|
|
28
|
-
uiLinkFor,
|
|
29
38
|
unwrapItem,
|
|
30
39
|
unwrapItems,
|
|
31
40
|
unwrapOperation,
|
|
41
|
+
unwrapQueryCount,
|
|
32
42
|
vendorId
|
|
33
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-NSW7JPVS.js";
|
|
34
44
|
import {
|
|
35
45
|
CAPABILITIES
|
|
36
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-ASMCTZO2.js";
|
|
37
47
|
|
|
38
48
|
// src/resources/appointments.ts
|
|
39
49
|
var AppointmentsResource = class extends BaseResource {
|
|
@@ -120,6 +130,50 @@ var AuthenticateResource = class {
|
|
|
120
130
|
}
|
|
121
131
|
};
|
|
122
132
|
|
|
133
|
+
// src/resources/billing_codes.ts
|
|
134
|
+
var BillingCodesResource = class extends BaseResource {
|
|
135
|
+
constructor(http, pageSize) {
|
|
136
|
+
super(http, {
|
|
137
|
+
key: "billing_codes",
|
|
138
|
+
resourcePath: "/V1.0/BillingCodes",
|
|
139
|
+
deletable: false,
|
|
140
|
+
udfCapable: false,
|
|
141
|
+
searchFields: ["name"]
|
|
142
|
+
}, pageSize);
|
|
143
|
+
}
|
|
144
|
+
/** Query BillingCode records with the G3 filter surface (server-side JSON filter). */
|
|
145
|
+
query(params) {
|
|
146
|
+
return this.queryItems(params);
|
|
147
|
+
}
|
|
148
|
+
/** Get every BillingCode record (the MCP-preferred read; 1000-page guarded). */
|
|
149
|
+
async queryAll(params) {
|
|
150
|
+
return this.queryAllItems(params);
|
|
151
|
+
}
|
|
152
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
153
|
+
async count(params) {
|
|
154
|
+
return this.countGlobal(params);
|
|
155
|
+
}
|
|
156
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
|
|
157
|
+
async get(id, opts = {}) {
|
|
158
|
+
return this.getGlobal(id, opts?.fresh === true);
|
|
159
|
+
}
|
|
160
|
+
/** Resolve a BillingCode from a numeric id (or, where the vendor indexes a primary text field,
|
|
161
|
+
* from an exact match on name). A numeric id miss throws NOT_FOUND; a complete
|
|
162
|
+
* non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
|
|
163
|
+
* cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
|
|
164
|
+
* The compact shape is the structural alias of the full record; `expand: true` is the
|
|
165
|
+
* type-level escape hatch. */
|
|
166
|
+
async resolve(identifier, opts = {}) {
|
|
167
|
+
void opts;
|
|
168
|
+
return this.resolveBase(identifier);
|
|
169
|
+
}
|
|
170
|
+
/** Search BillingCode records: bounded contains on name (no vendor free-text search exists);
|
|
171
|
+
* limit defaults to 25, max 500. */
|
|
172
|
+
async search(query, opts) {
|
|
173
|
+
return this.searchBase(query, { limit: opts?.limit });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
123
177
|
// src/resources/billing_item_approval_levels.ts
|
|
124
178
|
var BillingItemApprovalLevelsResource = class extends BaseResource {
|
|
125
179
|
constructor(http, pageSize) {
|
|
@@ -1096,7 +1150,8 @@ async function fetchRecord(http, path, label, readOpts) {
|
|
|
1096
1150
|
}
|
|
1097
1151
|
return body.item;
|
|
1098
1152
|
}
|
|
1099
|
-
async function moveConfigurationItem(_resource, sourceId, opts
|
|
1153
|
+
async function moveConfigurationItem(_resource, sourceId, opts) {
|
|
1154
|
+
const http = _resource.http;
|
|
1100
1155
|
const numericSource = parseIdentifier(sourceId, "sourceId");
|
|
1101
1156
|
if (opts?.destinationCompanyId === void 0) {
|
|
1102
1157
|
throw new AutotaskConfigError("moveConfigurationItem: destinationCompanyId is required");
|
|
@@ -1205,7 +1260,8 @@ async function ciScopedQuery(http, path, itemID, limit) {
|
|
|
1205
1260
|
});
|
|
1206
1261
|
return unwrapItems(res).items;
|
|
1207
1262
|
}
|
|
1208
|
-
async function profile(resource, id, opts = {}
|
|
1263
|
+
async function profile(resource, id, opts = {}) {
|
|
1264
|
+
const http = resource.http;
|
|
1209
1265
|
const noteLimit = profilePartLimit(opts.noteLimit, "noteLimit");
|
|
1210
1266
|
const relatedLimit = profilePartLimit(opts.relatedLimit, "relatedLimit");
|
|
1211
1267
|
const ci = await resource.get(id);
|
|
@@ -1333,11 +1389,11 @@ var ConfigurationItemsResource = class extends BaseResource {
|
|
|
1333
1389
|
}
|
|
1334
1390
|
/** Node-parity special op (hand-written in src/resources/special/configuration_items.ts). */
|
|
1335
1391
|
moveConfigurationItem(sourceId, opts) {
|
|
1336
|
-
return moveConfigurationItem(this, sourceId, opts
|
|
1392
|
+
return moveConfigurationItem(this, sourceId, opts);
|
|
1337
1393
|
}
|
|
1338
1394
|
/** Node-parity special op (hand-written in src/resources/special/configuration_items.ts). */
|
|
1339
1395
|
profile(id, opts) {
|
|
1340
|
-
return profile(this, id, opts
|
|
1396
|
+
return profile(this, id, opts);
|
|
1341
1397
|
}
|
|
1342
1398
|
};
|
|
1343
1399
|
|
|
@@ -1691,7 +1747,8 @@ async function fetchRecord2(http, path, label, readOpts) {
|
|
|
1691
1747
|
}
|
|
1692
1748
|
return body.item;
|
|
1693
1749
|
}
|
|
1694
|
-
async function moveToCompany(_resource, sourceContactId, destinationCompanyId, opts = {}
|
|
1750
|
+
async function moveToCompany(_resource, sourceContactId, destinationCompanyId, opts = {}) {
|
|
1751
|
+
const http = _resource.http;
|
|
1695
1752
|
const sourceId = parseIdentifier(sourceContactId, "sourceContactId");
|
|
1696
1753
|
const destId = parseIdentifier(destinationCompanyId, "destinationCompanyId");
|
|
1697
1754
|
const source = await fetchRecord2(http, `/V1.0/Contacts/${sourceId}`, `Contact ${sourceId}`, { maxRetries: opts.maxRetries });
|
|
@@ -1781,153 +1838,28 @@ async function moveToCompany(_resource, sourceContactId, destinationCompanyId, o
|
|
|
1781
1838
|
}
|
|
1782
1839
|
return { sourceId, newId, destinationCompanyId: destId, created: true, sourceDeleted };
|
|
1783
1840
|
}
|
|
1784
|
-
var
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
this.vendorFields = vendorFields;
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
|
-
async function upsertByEmail(resource, companyID, email, extra, opts, http) {
|
|
1810
|
-
const cid = parseIdentifier(companyID, "companyID");
|
|
1811
|
-
if (typeof email !== "string" || !EMAIL_SHAPE.test(email)) {
|
|
1812
|
-
throw new AutotaskConfigError(
|
|
1813
|
-
`upsertByEmail: email must be a non-empty string with the @ shape (a non-empty local part + a non-empty domain; got ${JSON.stringify(email)})`,
|
|
1814
|
-
{ operation: OP_UPSERT_EMAIL }
|
|
1815
|
-
);
|
|
1816
|
-
}
|
|
1817
|
-
if (extra !== void 0) {
|
|
1818
|
-
if (extra.isActive !== void 0 && typeof extra.isActive !== "boolean" && extra.isActive !== 0 && extra.isActive !== 1) {
|
|
1819
|
-
throw new AutotaskConfigError(
|
|
1820
|
-
`upsertByEmail: extra.isActive must be a boolean (mapped to the zone-correct number 1/0) or a raw 0/1 number (got ${String(extra.isActive)})`,
|
|
1821
|
-
{ operation: OP_UPSERT_EMAIL }
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
if (extra.firstName !== void 0 && (typeof extra.firstName !== "string" || extra.firstName === "")) {
|
|
1825
|
-
throw new AutotaskConfigError("upsertByEmail: extra.firstName must be a non-empty string (or omitted \u2014 the node defaults it to the email local-part)", { operation: OP_UPSERT_EMAIL });
|
|
1826
|
-
}
|
|
1827
|
-
if (extra.lastName !== void 0 && typeof extra.lastName !== "string") {
|
|
1828
|
-
throw new AutotaskConfigError("upsertByEmail: extra.lastName must be a string (or omitted \u2014 the node defaults it to the non-empty neutral value Unknown)", { operation: OP_UPSERT_EMAIL });
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
const parentRes = await http.request({ method: "GET", path: `/V1.0/Companies/${cid}`, operation: OP_UPSERT_EMAIL, maxRetries: opts.maxRetries });
|
|
1832
|
-
const parentBody = parentRes;
|
|
1833
|
-
if (parentBody === null || !("item" in parentBody) || parentBody.item === null || parentBody.item === void 0) {
|
|
1834
|
-
throw new CompanyNotFoundError(
|
|
1835
|
-
`upsertByEmail: company ${cid} was not found (companies.get returned item: null \u2014 the companies count/query surface is blind to this API user; get is the authority)`,
|
|
1836
|
-
{ resourceIds: [cid], details: { idRefs: [{ entity: "company", id: cid }] }, operation: OP_UPSERT_EMAIL }
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
const q = await resource.queryBounded({
|
|
1840
|
-
filter: [
|
|
1841
|
-
{ field: "emailAddress", op: "eq", value: email },
|
|
1842
|
-
{ field: "companyID", op: "eq", value: cid }
|
|
1843
|
-
],
|
|
1844
|
-
limit: DEDUP_LIMIT
|
|
1845
|
-
});
|
|
1846
|
-
const match = q.items[0];
|
|
1847
|
-
if (match !== void 0) {
|
|
1848
|
-
if (opts.dryRun === true) {
|
|
1849
|
-
return buildDryRunResult(
|
|
1850
|
-
{
|
|
1851
|
-
operation: OP_UPSERT_EMAIL,
|
|
1852
|
-
method: "POST",
|
|
1853
|
-
path: `/V1.0/Companies/${cid}/Contacts`,
|
|
1854
|
-
ids: [cid],
|
|
1855
|
-
checks: [
|
|
1856
|
-
{ name: "input-validation", ok: true },
|
|
1857
|
-
{ name: "parent-exists", ok: true },
|
|
1858
|
-
{ name: "duplicate-detection", ok: true }
|
|
1859
|
-
],
|
|
1860
|
-
impact: { affected: 0, scope: "no write \u2014 the contact already exists (dedup match; the create is skipped)", reversible: true },
|
|
1861
|
-
warnings: [`EXISTS: a contact with the address is already on company ${cid} (id ${Number(match.id)}) \u2014 the apply would skip the create (zero mutating wire).`],
|
|
1862
|
-
data: { reason: "EXISTS", id: Number(match.id) }
|
|
1863
|
-
},
|
|
1864
|
-
{ redactPersonalData: http.redactPersonalData }
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
return { id: Number(match.id), created: false, existing: match };
|
|
1868
|
-
}
|
|
1869
|
-
const localPart = email.split("@")[0];
|
|
1870
|
-
const isActive = extra?.isActive === void 0 ? true : extra.isActive;
|
|
1871
|
-
const body = {
|
|
1872
|
-
emailAddress: email,
|
|
1873
|
-
companyID: cid,
|
|
1874
|
-
firstName: extra?.firstName ?? localPart,
|
|
1875
|
-
lastName: extra?.lastName ?? "Unknown",
|
|
1876
|
-
isActive: typeof isActive === "boolean" ? isActive ? 1 : 0 : isActive
|
|
1877
|
-
};
|
|
1878
|
-
if (opts.dryRun === true) {
|
|
1879
|
-
return buildDryRunResult(
|
|
1880
|
-
{
|
|
1881
|
-
operation: OP_UPSERT_EMAIL,
|
|
1882
|
-
method: "POST",
|
|
1883
|
-
path: `/V1.0/Companies/${cid}/Contacts`,
|
|
1884
|
-
ids: [cid],
|
|
1885
|
-
checks: [
|
|
1886
|
-
{ name: "input-validation", ok: true },
|
|
1887
|
-
{ name: "parent-exists", ok: true },
|
|
1888
|
-
{ name: "duplicate-detection", ok: true }
|
|
1889
|
-
],
|
|
1890
|
-
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
1891
|
-
data: body
|
|
1892
|
-
},
|
|
1893
|
-
{ redactPersonalData: http.redactPersonalData }
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1896
|
-
let merges = 0;
|
|
1897
|
-
for (; ; ) {
|
|
1898
|
-
try {
|
|
1899
|
-
const created = await resource.create(cid, body, { maxRetries: opts.maxRetries });
|
|
1900
|
-
return { id: created.id, created: true };
|
|
1901
|
-
} catch (err) {
|
|
1902
|
-
if (!isAutotaskError(err) || err.vendorError === void 0) throw err;
|
|
1903
|
-
const named = (parseVendorErrors(err.vendorError) ?? []).map((e) => e.field).filter((f) => f !== void 0);
|
|
1904
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1905
|
-
const unique = [...named].filter((f) => seen.has(f) ? false : (seen.add(f), true));
|
|
1906
|
-
const hasDefault = unique.some((f) => SAFE_CONTACT_CREATE_DEFAULTS[f] !== void 0);
|
|
1907
|
-
if (hasDefault && merges < UPSERT_MAX_DISCOVERY_MERGES) {
|
|
1908
|
-
for (const f of unique) {
|
|
1909
|
-
if (body[f] === void 0 && SAFE_CONTACT_CREATE_DEFAULTS[f] !== void 0) {
|
|
1910
|
-
body[f] = SAFE_CONTACT_CREATE_DEFAULTS[f];
|
|
1911
|
-
merges += 1;
|
|
1912
|
-
break;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
const detail = formatVendorDetail(err.vendorError);
|
|
1918
|
-
throw new ContactCreateFailedError(
|
|
1919
|
-
`upsertByEmail: the vendor rejected the contact create${unique.length > 0 ? ` naming field(s) ${unique.join(", ")}` : ""} (no proven-safe default \u2014 the required set is fully filled by the node)${detail ? ` \u2014 ${detail}` : ""}`,
|
|
1920
|
-
unique,
|
|
1921
|
-
{
|
|
1922
|
-
operation: OP_UPSERT_EMAIL,
|
|
1923
|
-
httpStatus: err.httpStatus,
|
|
1924
|
-
vendorError: err.vendorError,
|
|
1925
|
-
resourceIds: [cid],
|
|
1926
|
-
details: { idRefs: [{ entity: "company", id: cid }] }
|
|
1927
|
-
}
|
|
1928
|
-
);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1841
|
+
var FULL_PROFILE_OP = "contacts.fullProfile";
|
|
1842
|
+
async function fullProfile(resource, id, opts = {}) {
|
|
1843
|
+
const client = clientOf(resource, FULL_PROFILE_OP);
|
|
1844
|
+
const ticketLimit = compositePartLimit(opts.ticketLimit, "ticketLimit", FULL_PROFILE_OP);
|
|
1845
|
+
const contactId = parseIdentifier(id, "id");
|
|
1846
|
+
const envelope = await invokeRead(client, "contacts.query", [
|
|
1847
|
+
{ filter: [{ field: "id", op: "eq", value: contactId }], limit: 1 }
|
|
1848
|
+
]);
|
|
1849
|
+
const row = (envelope.items ?? [])[0];
|
|
1850
|
+
if (row === void 0) {
|
|
1851
|
+
throw new NotFoundError(`Contact ${contactId} not found`, { operation: "contacts.get", httpStatus: 404 });
|
|
1852
|
+
}
|
|
1853
|
+
const fk = typeof row.companyID === "number" && row.companyID > 0 ? row.companyID : null;
|
|
1854
|
+
const root = fk === null ? row : await invokeRead(client, "contacts.get", [fk, contactId]);
|
|
1855
|
+
const company = fk === null ? { value: null, absent: "NO_FK" } : await compositePart(client, "companies.get", [fk]);
|
|
1856
|
+
const openTickets = await openTicketsOfPart(
|
|
1857
|
+
client,
|
|
1858
|
+
"contactID",
|
|
1859
|
+
contactId,
|
|
1860
|
+
ticketLimit
|
|
1861
|
+
);
|
|
1862
|
+
return { root, parts: { company, open_tickets: openTickets } };
|
|
1931
1863
|
}
|
|
1932
1864
|
|
|
1933
1865
|
// src/resources/contacts.ts
|
|
@@ -2014,11 +1946,11 @@ var ContactsResource = class extends BaseResource {
|
|
|
2014
1946
|
}
|
|
2015
1947
|
/** Node-parity special op (hand-written in src/resources/special/contacts.ts). */
|
|
2016
1948
|
moveToCompany(sourceContactId, destinationCompanyId, opts = {}) {
|
|
2017
|
-
return moveToCompany(this, sourceContactId, destinationCompanyId, opts
|
|
1949
|
+
return moveToCompany(this, sourceContactId, destinationCompanyId, opts);
|
|
2018
1950
|
}
|
|
2019
1951
|
/** Node-parity special op (hand-written in src/resources/special/contacts.ts). */
|
|
2020
|
-
|
|
2021
|
-
return
|
|
1952
|
+
fullProfile(id, opts) {
|
|
1953
|
+
return fullProfile(this, id, opts);
|
|
2022
1954
|
}
|
|
2023
1955
|
};
|
|
2024
1956
|
|
|
@@ -3861,7 +3793,8 @@ var PATH_SUFFIX = {
|
|
|
3861
3793
|
remove: "StockedItemsRemove",
|
|
3862
3794
|
transfer: "StockedItemsTransfer"
|
|
3863
3795
|
};
|
|
3864
|
-
async function stockedItems(_resource, kind, parentId, data, opts = {}
|
|
3796
|
+
async function stockedItems(_resource, kind, parentId, data, opts = {}) {
|
|
3797
|
+
const http = _resource.http;
|
|
3865
3798
|
const numericParent = parseIdentifier(parentId, "parentId");
|
|
3866
3799
|
const suffix = PATH_SUFFIX[kind];
|
|
3867
3800
|
if (opts.dryRun === true) {
|
|
@@ -3954,15 +3887,15 @@ var InventoryProductsResource = class extends BaseResource {
|
|
|
3954
3887
|
}
|
|
3955
3888
|
/** Node-parity special op (hand-written in src/resources/special/inventory_products.ts). */
|
|
3956
3889
|
stockedItemsAdd(parentId, data, opts = {}) {
|
|
3957
|
-
return stockedItems(this, "add", parentId, data, opts
|
|
3890
|
+
return stockedItems(this, "add", parentId, data, opts);
|
|
3958
3891
|
}
|
|
3959
3892
|
/** Node-parity special op (hand-written in src/resources/special/inventory_products.ts). */
|
|
3960
3893
|
stockedItemsRemove(parentId, data, opts = {}) {
|
|
3961
|
-
return stockedItems(this, "remove", parentId, data, opts
|
|
3894
|
+
return stockedItems(this, "remove", parentId, data, opts);
|
|
3962
3895
|
}
|
|
3963
3896
|
/** Node-parity special op (hand-written in src/resources/special/inventory_products.ts). */
|
|
3964
3897
|
stockedItemsTransfer(parentId, data, opts = {}) {
|
|
3965
|
-
return stockedItems(this, "transfer", parentId, data, opts
|
|
3898
|
+
return stockedItems(this, "transfer", parentId, data, opts);
|
|
3966
3899
|
}
|
|
3967
3900
|
};
|
|
3968
3901
|
|
|
@@ -5000,6 +4933,19 @@ var QuoteLocationsResource = class extends BaseResource {
|
|
|
5000
4933
|
}
|
|
5001
4934
|
};
|
|
5002
4935
|
|
|
4936
|
+
// src/resources/special/quotes.ts
|
|
4937
|
+
var OP = "quotes.withItems";
|
|
4938
|
+
async function withItems(resource, id, opts = {}) {
|
|
4939
|
+
const client = clientOf(resource, OP);
|
|
4940
|
+
const itemLimit = compositePartLimit(opts.itemLimit, "itemLimit", OP);
|
|
4941
|
+
const quoteId = parseIdentifier(id, "id");
|
|
4942
|
+
const root = await invokeRead(client, "quotes.get", [quoteId]);
|
|
4943
|
+
const items = await compositePart(client, "quote_items.query", [
|
|
4944
|
+
{ filter: [{ field: "quoteID", op: "eq", value: quoteId }], limit: itemLimit }
|
|
4945
|
+
]);
|
|
4946
|
+
return { root, parts: { items } };
|
|
4947
|
+
}
|
|
4948
|
+
|
|
5003
4949
|
// src/resources/quotes.ts
|
|
5004
4950
|
var QuotesResource = class extends BaseResource {
|
|
5005
4951
|
constructor(http, pageSize) {
|
|
@@ -5065,6 +5011,10 @@ var QuotesResource = class extends BaseResource {
|
|
|
5065
5011
|
async search(query, opts) {
|
|
5066
5012
|
return this.searchBase(query, { limit: opts?.limit });
|
|
5067
5013
|
}
|
|
5014
|
+
/** Node-parity special op (hand-written in src/resources/special/quotes.ts). */
|
|
5015
|
+
withItems(id, opts) {
|
|
5016
|
+
return withItems(this, id, opts);
|
|
5017
|
+
}
|
|
5068
5018
|
};
|
|
5069
5019
|
|
|
5070
5020
|
// src/resources/quote_templates.ts
|
|
@@ -5263,8 +5213,152 @@ var ResourceSkillsResource = class extends BaseResource {
|
|
|
5263
5213
|
}
|
|
5264
5214
|
};
|
|
5265
5215
|
|
|
5216
|
+
// src/resources/special/resources.ts
|
|
5217
|
+
async function whoAmI(_resource, userName) {
|
|
5218
|
+
const http = _resource.http;
|
|
5219
|
+
const user = userName.trim();
|
|
5220
|
+
if (user === "") throw new AutotaskConfigError("whoAmI: the configured UserName is empty");
|
|
5221
|
+
const value = user.includes("@") ? user.split("@")[0] ?? "" : user;
|
|
5222
|
+
const res = await http.request({
|
|
5223
|
+
method: "POST",
|
|
5224
|
+
idempotent: true,
|
|
5225
|
+
path: "/V1.0/Resources/query",
|
|
5226
|
+
body: { MaxRecords: 2, filter: [{ field: "userName", op: "eq", value }] },
|
|
5227
|
+
operation: "resources.whoAmI"
|
|
5228
|
+
});
|
|
5229
|
+
const matches = unwrapItems(res).items;
|
|
5230
|
+
if (matches.length === 0) return null;
|
|
5231
|
+
if (matches.length > 1) {
|
|
5232
|
+
const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
|
|
5233
|
+
throw new ResolutionError(
|
|
5234
|
+
`whoAmI: "${value}" is ambiguous (matched ${matches.length} resources)`,
|
|
5235
|
+
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: "resource", id: cid })) } }
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
5238
|
+
return matches[0];
|
|
5239
|
+
}
|
|
5240
|
+
var WORKLOAD_OP = "resources.workload";
|
|
5241
|
+
async function workload(resource, id, opts = {}) {
|
|
5242
|
+
const client = clientOf(resource, WORKLOAD_OP);
|
|
5243
|
+
const ticketLimit = compositePartLimit(opts.ticketLimit, "ticketLimit", WORKLOAD_OP);
|
|
5244
|
+
const taskLimit = compositePartLimit(opts.taskLimit, "taskLimit", WORKLOAD_OP);
|
|
5245
|
+
const timeOffLimit = compositePartLimit(opts.timeOffLimit, "timeOffLimit", WORKLOAD_OP);
|
|
5246
|
+
const resourceId = parseIdentifier(id, "id");
|
|
5247
|
+
const root = await invokeRead(client, "resources.get", [resourceId]);
|
|
5248
|
+
const strict = { allowAssumed: false };
|
|
5249
|
+
const joined = await compositePart(client, "tickets.forResource", [
|
|
5250
|
+
resourceId,
|
|
5251
|
+
{ openOnly: true, limit: Math.min(ticketLimit, 500), ...strict }
|
|
5252
|
+
]);
|
|
5253
|
+
const counted = await compositePart(client, "tickets.forResource", [resourceId, { countOnly: true, ...strict }]);
|
|
5254
|
+
const assignments = await compositePart(client, "service_call_task_resources.query", [
|
|
5255
|
+
{ filter: [{ field: "resourceID", op: "eq", value: resourceId }], limit: taskLimit }
|
|
5256
|
+
]);
|
|
5257
|
+
const balances = await compositePart(client, "resource_time_off_balances.getByResource", [resourceId]);
|
|
5258
|
+
const timeOff = await compositePart(client, "time_off_requests.query", [
|
|
5259
|
+
{ filter: [{ field: "resourceID", op: "eq", value: resourceId }], limit: timeOffLimit }
|
|
5260
|
+
]);
|
|
5261
|
+
const openTickets = joined.value !== null && typeof joined.value === "object" ? counted.value !== null && typeof counted.value === "object" ? (() => {
|
|
5262
|
+
const { primary, secondary, both, open, capped } = counted.value;
|
|
5263
|
+
return {
|
|
5264
|
+
...joined,
|
|
5265
|
+
value: { ...joined.value, totals: { primary, secondary, both, open, capped } }
|
|
5266
|
+
};
|
|
5267
|
+
})() : {
|
|
5268
|
+
...joined,
|
|
5269
|
+
value: { ...joined.value, totalsAbsent: counted.absent ?? "NO_TOTALS" }
|
|
5270
|
+
} : joined;
|
|
5271
|
+
const taskIds = numericIds(
|
|
5272
|
+
assignments.value?.items ?? [],
|
|
5273
|
+
"serviceCallTaskID"
|
|
5274
|
+
).slice(0, IN_FILTER_CAP);
|
|
5275
|
+
const taskDetails = taskIds.length === 0 ? { value: null, absent: "NO_ASSIGNMENTS" } : await compositePart(client, "service_call_tasks.query", [
|
|
5276
|
+
{ filter: [{ field: "taskID", op: "in", value: taskIds }], limit: taskLimit }
|
|
5277
|
+
]);
|
|
5278
|
+
return {
|
|
5279
|
+
root,
|
|
5280
|
+
parts: {
|
|
5281
|
+
open_tickets: openTickets,
|
|
5282
|
+
task_assignments: assignments,
|
|
5283
|
+
task_details: taskDetails,
|
|
5284
|
+
time_off_balances: balances,
|
|
5285
|
+
time_off_requests: timeOff
|
|
5286
|
+
}
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
5289
|
+
|
|
5290
|
+
// src/resources/resources.ts
|
|
5291
|
+
var ResourcesResource = class extends BaseResource {
|
|
5292
|
+
clientConfig;
|
|
5293
|
+
constructor(http, clientConfig, pageSize) {
|
|
5294
|
+
super(http, {
|
|
5295
|
+
key: "resources",
|
|
5296
|
+
resourcePath: "/V1.0/Resources",
|
|
5297
|
+
deletable: false,
|
|
5298
|
+
udfCapable: false,
|
|
5299
|
+
collectionUpdate: true,
|
|
5300
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
5301
|
+
searchFields: ["firstName", "lastName"]
|
|
5302
|
+
}, pageSize);
|
|
5303
|
+
this.clientConfig = clientConfig;
|
|
5304
|
+
}
|
|
5305
|
+
/** Query Resource records with the G3 filter surface (server-side JSON filter). */
|
|
5306
|
+
query(params) {
|
|
5307
|
+
return this.queryItems(params);
|
|
5308
|
+
}
|
|
5309
|
+
/** Get every Resource record (the MCP-preferred read; 1000-page guarded). */
|
|
5310
|
+
async queryAll(params) {
|
|
5311
|
+
return this.queryAllItems(params);
|
|
5312
|
+
}
|
|
5313
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
5314
|
+
async count(params) {
|
|
5315
|
+
return this.countGlobal(params);
|
|
5316
|
+
}
|
|
5317
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
|
|
5318
|
+
async get(id, opts = {}) {
|
|
5319
|
+
return this.getGlobal(id, opts?.fresh === true);
|
|
5320
|
+
}
|
|
5321
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
5322
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
5323
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
5324
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
5325
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
5326
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
5327
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
5328
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
5329
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
5330
|
+
async update(id, data, opts = {}) {
|
|
5331
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
5332
|
+
return this.updateGlobal(id, data, opts);
|
|
5333
|
+
}
|
|
5334
|
+
/** Resolve a Resource from a numeric id (or, where the vendor indexes a primary text field,
|
|
5335
|
+
* from an exact match on firstName / lastName). A numeric id miss throws NOT_FOUND; a complete
|
|
5336
|
+
* non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
|
|
5337
|
+
* cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
|
|
5338
|
+
* The compact shape is the structural alias of the full record; `expand: true` is the
|
|
5339
|
+
* type-level escape hatch. */
|
|
5340
|
+
async resolve(identifier, opts = {}) {
|
|
5341
|
+
void opts;
|
|
5342
|
+
return this.resolveBase(identifier);
|
|
5343
|
+
}
|
|
5344
|
+
/** Search Resource records: bounded contains on firstName + lastName (no vendor free-text search exists);
|
|
5345
|
+
* limit defaults to 25, max 500. */
|
|
5346
|
+
async search(query, opts) {
|
|
5347
|
+
return this.searchBase(query, { limit: opts?.limit });
|
|
5348
|
+
}
|
|
5349
|
+
/** Node-parity special op (hand-written in src/resources/special/resources.ts). */
|
|
5350
|
+
whoAmI() {
|
|
5351
|
+
return whoAmI(this, this.clientConfig.userName);
|
|
5352
|
+
}
|
|
5353
|
+
/** Node-parity special op (hand-written in src/resources/special/resources.ts). */
|
|
5354
|
+
workload(id, opts) {
|
|
5355
|
+
return workload(this, id, opts);
|
|
5356
|
+
}
|
|
5357
|
+
};
|
|
5358
|
+
|
|
5266
5359
|
// src/resources/special/resource_time_off_additional.ts
|
|
5267
|
-
async function getByResource(_resource, resourceId
|
|
5360
|
+
async function getByResource(_resource, resourceId) {
|
|
5361
|
+
const http = _resource.http;
|
|
5268
5362
|
const numericResource = parseIdentifier(resourceId, "resourceId");
|
|
5269
5363
|
const res = await http.request({
|
|
5270
5364
|
method: "GET",
|
|
@@ -5317,7 +5411,7 @@ var ResourceTimeOffAdditionalResource = class extends BaseResource {
|
|
|
5317
5411
|
}
|
|
5318
5412
|
/** Node-parity special op (hand-written in src/resources/special/resource_time_off_additional.ts). */
|
|
5319
5413
|
getByResource(resourceId) {
|
|
5320
|
-
return getByResource(this, resourceId
|
|
5414
|
+
return getByResource(this, resourceId);
|
|
5321
5415
|
}
|
|
5322
5416
|
};
|
|
5323
5417
|
|
|
@@ -5364,7 +5458,8 @@ var ResourceTimeOffApproversResource = class extends BaseResource {
|
|
|
5364
5458
|
};
|
|
5365
5459
|
|
|
5366
5460
|
// src/resources/special/resource_time_off_balances.ts
|
|
5367
|
-
async function getByResource2(_resource, resourceId
|
|
5461
|
+
async function getByResource2(_resource, resourceId) {
|
|
5462
|
+
const http = _resource.http;
|
|
5368
5463
|
const numericResource = parseIdentifier(resourceId, "resourceId");
|
|
5369
5464
|
const res = await http.request({
|
|
5370
5465
|
method: "GET",
|
|
@@ -5373,7 +5468,8 @@ async function getByResource2(_resource, resourceId, http) {
|
|
|
5373
5468
|
});
|
|
5374
5469
|
return unwrapItems(res, "resource_time_off_balances.getByResource").items;
|
|
5375
5470
|
}
|
|
5376
|
-
async function getByYear(_resource, resourceId, year
|
|
5471
|
+
async function getByYear(_resource, resourceId, year) {
|
|
5472
|
+
const http = _resource.http;
|
|
5377
5473
|
const numericResource = parseIdentifier(resourceId, "resourceId");
|
|
5378
5474
|
if (!Number.isSafeInteger(year) || year < 1) {
|
|
5379
5475
|
throw new AutotaskConfigError("getByYear: year must be a positive integer");
|
|
@@ -5416,11 +5512,11 @@ var ResourceTimeOffBalancesResource = class extends BaseResource {
|
|
|
5416
5512
|
}
|
|
5417
5513
|
/** Node-parity special op (hand-written in src/resources/special/resource_time_off_balances.ts). */
|
|
5418
5514
|
getByResource(resourceId) {
|
|
5419
|
-
return getByResource2(this, resourceId
|
|
5515
|
+
return getByResource2(this, resourceId);
|
|
5420
5516
|
}
|
|
5421
5517
|
/** Node-parity special op (hand-written in src/resources/special/resource_time_off_balances.ts). */
|
|
5422
5518
|
getByYear(resourceId, year) {
|
|
5423
|
-
return getByYear(this, resourceId, year
|
|
5519
|
+
return getByYear(this, resourceId, year);
|
|
5424
5520
|
}
|
|
5425
5521
|
};
|
|
5426
5522
|
|
|
@@ -5534,6 +5630,37 @@ var SalesOrdersResource = class extends BaseResource {
|
|
|
5534
5630
|
}
|
|
5535
5631
|
};
|
|
5536
5632
|
|
|
5633
|
+
// src/resources/special/service_calls.ts
|
|
5634
|
+
var OP2 = "service_calls.fullProfile";
|
|
5635
|
+
async function fullProfile2(resource, id, opts = {}) {
|
|
5636
|
+
const client = clientOf(resource, OP2);
|
|
5637
|
+
const ticketLimit = compositePartLimit(opts.ticketLimit, "ticketLimit", OP2);
|
|
5638
|
+
const taskLimit = compositePartLimit(opts.taskLimit, "taskLimit", OP2);
|
|
5639
|
+
const callId = parseIdentifier(id, "id");
|
|
5640
|
+
const root = await invokeRead(client, "service_calls.get", [callId]);
|
|
5641
|
+
const tickets = await compositePart(client, "service_call_tickets.query", [
|
|
5642
|
+
{ filter: [{ field: "serviceCallID", op: "eq", value: callId }], limit: ticketLimit }
|
|
5643
|
+
]);
|
|
5644
|
+
const tasks = await compositePart(client, "service_call_tasks.query", [
|
|
5645
|
+
{ filter: [{ field: "serviceCallID", op: "eq", value: callId }], limit: taskLimit }
|
|
5646
|
+
]);
|
|
5647
|
+
const taskIds = numericIds(
|
|
5648
|
+
tasks.value?.items ?? [],
|
|
5649
|
+
"taskID"
|
|
5650
|
+
).slice(0, IN_FILTER_CAP);
|
|
5651
|
+
const resourceAssignments = taskIds.length === 0 ? { value: null, absent: "NO_TASKS" } : await compositePart(client, "service_call_task_resources.query", [
|
|
5652
|
+
{ filter: [{ field: "taskID", op: "in", value: taskIds }], limit: taskLimit }
|
|
5653
|
+
]);
|
|
5654
|
+
return {
|
|
5655
|
+
root,
|
|
5656
|
+
parts: {
|
|
5657
|
+
tickets,
|
|
5658
|
+
tasks,
|
|
5659
|
+
resource_assignments: resourceAssignments
|
|
5660
|
+
}
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5663
|
+
|
|
5537
5664
|
// src/resources/service_calls.ts
|
|
5538
5665
|
var ServiceCallsResource = class extends BaseResource {
|
|
5539
5666
|
constructor(http, pageSize) {
|
|
@@ -5601,6 +5728,10 @@ var ServiceCallsResource = class extends BaseResource {
|
|
|
5601
5728
|
void opts;
|
|
5602
5729
|
return this.resolveBase(identifier);
|
|
5603
5730
|
}
|
|
5731
|
+
/** Node-parity special op (hand-written in src/resources/special/service_calls.ts). */
|
|
5732
|
+
fullProfile(id, opts) {
|
|
5733
|
+
return fullProfile2(this, id, opts);
|
|
5734
|
+
}
|
|
5604
5735
|
};
|
|
5605
5736
|
|
|
5606
5737
|
// src/resources/service_call_task_resources.ts
|
|
@@ -6893,23 +7024,203 @@ var TicketChangeRequestApprovalsResource = class extends BaseResource {
|
|
|
6893
7024
|
}
|
|
6894
7025
|
};
|
|
6895
7026
|
|
|
7027
|
+
// src/resources/special/_shared/move-engine.ts
|
|
7028
|
+
import { randomUUID } from "crypto";
|
|
7029
|
+
var MOVE_SOURCE_AGE_DEFAULT_DAYS = 14;
|
|
7030
|
+
var MOVE_EFFECTS_NOTE = "Create then delete: the new record gets a fresh create date and creator; creating/deleting may trigger notifications, SLA and workflow rules the API cannot suppress; vendor acceptance of the create is not validated by a dry run";
|
|
7031
|
+
var refuse = (code, message, ids, operation, extra = {}) => new MoveRefusedError(message, { code, operation, details: { status: "failed", ...ids, ...extra } });
|
|
7032
|
+
async function queryRows(http, resource, filter, operation, retries) {
|
|
7033
|
+
const res = await http.request({ method: "POST", idempotent: true, path: `/V1.0/${resource}/query`, body: { MaxRecords: 5, filter }, operation, maxRetries: retries });
|
|
7034
|
+
return unwrapItems(res).items;
|
|
7035
|
+
}
|
|
7036
|
+
function sameValue(a, b) {
|
|
7037
|
+
if (a === null || a === void 0 || a === "") return b === null || b === void 0 || b === "";
|
|
7038
|
+
if (b === null || b === void 0 || b === "") return false;
|
|
7039
|
+
if (typeof a === "number" || typeof b === "number") return Math.abs(Number(a) - Number(b)) < 1e-9;
|
|
7040
|
+
if (typeof a === "boolean" || typeof b === "boolean") return a === b;
|
|
7041
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
7042
|
+
const iso2 = /^\d{4}-\d{2}-\d{2}T/;
|
|
7043
|
+
if (iso2.test(a) && iso2.test(b)) return Date.parse(a) === Date.parse(b);
|
|
7044
|
+
return a.trim() === b.trim();
|
|
7045
|
+
}
|
|
7046
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
7047
|
+
}
|
|
7048
|
+
async function readTicket(http, id, retries, operation) {
|
|
7049
|
+
try {
|
|
7050
|
+
const res = await http.request({ method: "GET", path: `/V1.0/Tickets/${id}`, operation, maxRetries: retries });
|
|
7051
|
+
const item = res?.item;
|
|
7052
|
+
return item ?? null;
|
|
7053
|
+
} catch (e) {
|
|
7054
|
+
if (isAutotaskError(e) && (e.httpStatus === 404 || e.code === "NOT_FOUND")) return null;
|
|
7055
|
+
throw e;
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
async function preview(http, spec, sourceId, toTicketId, opts) {
|
|
7059
|
+
const op = spec.operation;
|
|
7060
|
+
const ids = { sourceId, toTicketId };
|
|
7061
|
+
const source = await spec.readSource(http, sourceId, opts.maxRetries);
|
|
7062
|
+
if (source === null) throw refuse("MOVE_SOURCE_NOT_FOUND", `${op}: source ${spec.entity} ${sourceId} does not exist`, ids, op);
|
|
7063
|
+
const sourceTicketId = spec.sourceTicketId(source);
|
|
7064
|
+
if (typeof sourceTicketId !== "number") {
|
|
7065
|
+
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${op}: source ${spec.entity} ${sourceId} is not attached to a ticket (task and internal records are not moved)`, ids, op);
|
|
7066
|
+
}
|
|
7067
|
+
if (sourceTicketId === toTicketId) throw refuse("MOVE_TARGET_SAME", `${op}: target ticket ${toTicketId} is the source's current ticket`, ids, op);
|
|
7068
|
+
if (opts.lastModifiedDate !== void 0) {
|
|
7069
|
+
const current = source.lastModifiedDateTime ?? source.lastModifiedDate;
|
|
7070
|
+
if (current !== opts.lastModifiedDate) {
|
|
7071
|
+
throw refuse("STALE_OBJECT", `${op}: source ${spec.entity} ${sourceId} was modified since the snapshot`, ids, op);
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
const cap = opts.maxSourceAgeDays === void 0 ? MOVE_SOURCE_AGE_DEFAULT_DAYS : Math.min(MOVE_SOURCE_AGE_DEFAULT_DAYS, opts.maxSourceAgeDays);
|
|
7075
|
+
if (!Number.isFinite(cap) || cap < 0) throw new AutotaskConfigError(`${op}: maxSourceAgeDays must be a number >= 0`);
|
|
7076
|
+
const when = Date.parse(String(source[spec.dateField] ?? ""));
|
|
7077
|
+
if (!Number.isFinite(when) || Date.now() - when > cap * 864e5) {
|
|
7078
|
+
throw refuse("MOVE_SOURCE_TOO_OLD", `${op}: source ${spec.entity} ${sourceId} is older than ${cap} day(s) (or undated) \u2014 move it in the Autotask UI`, ids, op, { manualAction: manualFor(http, spec, source, "older than the move age limit") });
|
|
7079
|
+
}
|
|
7080
|
+
const target = await readTicket(http, toTicketId, opts.maxRetries, op);
|
|
7081
|
+
if (target === null) throw refuse("MOVE_TARGET_MISSING", `${op}: target ticket ${toTicketId} does not exist`, ids, op);
|
|
7082
|
+
const closedStatus = await resolveTicketStatusSemantics(http);
|
|
7083
|
+
if (isClosedStatus(target.status, closedStatus.closedIds) || target.completedDate !== void 0 && target.completedDate !== null) {
|
|
7084
|
+
throw refuse(
|
|
7085
|
+
"MOVE_TARGET_CLOSED",
|
|
7086
|
+
`${op}: target ticket ${toTicketId} is closed (status in [${closedStatus.closedIds.join(", ")}] or completed) \u2014 closed ids ${closedStatus.source === "assumed" ? "ASSUMED (the tenant status picklist could not be read)" : "resolved from the tenant Tickets status picklist"}`,
|
|
7087
|
+
ids,
|
|
7088
|
+
op,
|
|
7089
|
+
{ closedStatusIds: closedStatus.closedIds, closedStatusSource: closedStatus.source }
|
|
7090
|
+
);
|
|
7091
|
+
}
|
|
7092
|
+
const origin = await readTicket(http, sourceTicketId, opts.maxRetries, op);
|
|
7093
|
+
const facts = {
|
|
7094
|
+
closedStatus: { closedIds: closedStatus.closedIds, source: closedStatus.source, note: closedStatus.note },
|
|
7095
|
+
sourceTicketId,
|
|
7096
|
+
sourceCompanyID: origin?.companyID ?? null,
|
|
7097
|
+
targetCompanyID: target.companyID ?? null,
|
|
7098
|
+
companyChanged: origin === null ? null : origin.companyID !== target.companyID,
|
|
7099
|
+
contractChanged: origin === null ? null : (origin.contractID ?? null) !== (target.contractID ?? null)
|
|
7100
|
+
};
|
|
7101
|
+
const ctx = { http, spec, sourceId, source, sourceTicketId, toTicketId, facts, retries: opts.maxRetries };
|
|
7102
|
+
await spec.checkSource(ctx);
|
|
7103
|
+
const sameScope = facts.companyChanged === false && facts.contractChanged === false;
|
|
7104
|
+
const payload = spec.buildPayload(source, toTicketId, { sameScope });
|
|
7105
|
+
await assertNoTwin(ctx, payload);
|
|
7106
|
+
return { ctx, payload };
|
|
7107
|
+
}
|
|
7108
|
+
function manualFor(http, spec, source, reason) {
|
|
7109
|
+
return { action: "delete_source", uiLink: spec.manualLink(http, source), reason };
|
|
7110
|
+
}
|
|
7111
|
+
async function assertNoTwin(ctx, payload) {
|
|
7112
|
+
const q = ctx.spec.twinQuery(payload);
|
|
7113
|
+
const rows = await queryRows(ctx.http, q.resource, q.filter, ctx.spec.operation, ctx.retries);
|
|
7114
|
+
if (rows.length > 0) {
|
|
7115
|
+
throw refuse("MOVE_TARGET_HAS_TWIN", `${ctx.spec.operation}: target ticket ${ctx.toTicketId} already has an equivalent ${ctx.spec.entity} (id ${String(rows[0].id)}) \u2014 a duplicate or resumed move`, { sourceId: ctx.sourceId, toTicketId: ctx.toTicketId }, ctx.spec.operation, { twinId: rows[0].id });
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
var statusOf = (e) => isAutotaskError(e) ? e.httpStatus : void 0;
|
|
7119
|
+
async function runMove(http, spec, sourceIdIn, toTicketIn, opts = {}) {
|
|
7120
|
+
const op = spec.operation;
|
|
7121
|
+
const sourceId = Number(parseIdentifier(sourceIdIn, "sourceId"));
|
|
7122
|
+
const toTicketId = Number(parseIdentifier(toTicketIn, "toTicketId"));
|
|
7123
|
+
if (!Number.isSafeInteger(sourceId) || !Number.isSafeInteger(toTicketId)) {
|
|
7124
|
+
throw new AutotaskConfigError(`${op}: sourceId and toTicketId must be safe integers`);
|
|
7125
|
+
}
|
|
7126
|
+
const { ctx, payload } = await preview(http, spec, sourceId, toTicketId, opts);
|
|
7127
|
+
if (opts.dryRun === true) {
|
|
7128
|
+
return buildDryRunResult(
|
|
7129
|
+
{
|
|
7130
|
+
operation: op,
|
|
7131
|
+
method: "POST",
|
|
7132
|
+
path: spec.createPath(toTicketId),
|
|
7133
|
+
ids: [sourceId],
|
|
7134
|
+
checks: [
|
|
7135
|
+
{ name: "source-exists", ok: true },
|
|
7136
|
+
{ name: "target-open", ok: true },
|
|
7137
|
+
{ name: "invariants", ok: true },
|
|
7138
|
+
{ name: "no-twin-on-target", ok: true }
|
|
7139
|
+
],
|
|
7140
|
+
impact: { affected: 2, scope: "create-then-delete", reversible: false },
|
|
7141
|
+
warnings: [MOVE_EFFECTS_NOTE, ...ctx.facts.companyChanged === true ? ["the target ticket belongs to a different company"] : []],
|
|
7142
|
+
data: payload
|
|
7143
|
+
},
|
|
7144
|
+
{ redactPersonalData: http.redactPersonalData }
|
|
7145
|
+
);
|
|
7146
|
+
}
|
|
7147
|
+
const correlationId = randomUUID();
|
|
7148
|
+
const ids = { sourceId, toTicketId, correlationId };
|
|
7149
|
+
await assertNoTwin(ctx, payload);
|
|
7150
|
+
let newId;
|
|
7151
|
+
try {
|
|
7152
|
+
const created = await http.request({ method: "POST", path: spec.createPath(toTicketId), body: payload, operation: op, maxRetries: 0 });
|
|
7153
|
+
newId = Number(unwrapOperation(created, op).id);
|
|
7154
|
+
} catch (e) {
|
|
7155
|
+
const status = statusOf(e);
|
|
7156
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
7157
|
+
throw new MoveRefusedError(`${op}: the vendor refused the create \u2014 nothing changed`, { code: "MOVE_CREATE_REFUSED", operation: op, httpStatus: status, details: { status: "failed", ...ids } });
|
|
7158
|
+
}
|
|
7159
|
+
throw new MoveRefusedError(`${op}: the create outcome is unknown (never resent) \u2014 check the target ticket for a new ${spec.entity} before retrying; the twin check blocks a second create`, { code: "MOVE_UNKNOWN", operation: op, details: { status: "unknown", ...ids } });
|
|
7160
|
+
}
|
|
7161
|
+
const rollback = async (code, message) => {
|
|
7162
|
+
try {
|
|
7163
|
+
await http.request({ method: "DELETE", path: spec.deletePath(newId, toTicketId), operation: op, maxRetries: 0 });
|
|
7164
|
+
} catch (re) {
|
|
7165
|
+
const unreadable = statusOf(re) === void 0 || (statusOf(re) ?? 0) >= 500;
|
|
7166
|
+
throw new MoveRefusedError(`${op}: ${message}; rolling back the new ${spec.entity} ${newId} FAILED \u2014 both records exist`, { code: unreadable ? "MOVE_UNKNOWN" : "MOVE_PARTIAL", operation: op, details: { status: unreadable ? "unknown" : "partial", ...ids, newId, reason: code } });
|
|
7167
|
+
}
|
|
7168
|
+
throw new MoveRefusedError(`${op}: ${message}; the new ${spec.entity} ${newId} was rolled back, the source is unchanged`, { code, operation: op, details: { status: "failed", ...ids, newId } });
|
|
7169
|
+
};
|
|
7170
|
+
const fresh = await spec.readById(http, newId, toTicketId).catch(() => null);
|
|
7171
|
+
if (fresh === null) return rollback("MOVE_VERIFY_FAILED", "the new record could not be read back");
|
|
7172
|
+
const mismatched = [...spec.verifyFields, "ticketID"].filter((f) => !sameValue(f === "ticketID" ? toTicketId : payload[f], fresh[f]));
|
|
7173
|
+
if (mismatched.length > 0) return rollback("MOVE_VERIFY_FAILED", `the read-back differs on ${mismatched.join(", ")}`);
|
|
7174
|
+
const again = await spec.readSource(http, sourceId, void 0).catch(() => null);
|
|
7175
|
+
if (again === null) {
|
|
7176
|
+
return { status: "moved_source_absent", sourceId, newId, correlationId, snapshot: ctx.source, facts: ctx.facts };
|
|
7177
|
+
}
|
|
7178
|
+
const changed = [...spec.guardFields].filter((f) => !sameValue(ctx.source[f], again[f]));
|
|
7179
|
+
if (changed.length > 0) return rollback("MOVE_SOURCE_CHANGED", `the source changed after the preview (${changed.join(", ")})`);
|
|
7180
|
+
try {
|
|
7181
|
+
await http.request({ method: "DELETE", path: spec.deletePath(sourceId, ctx.sourceTicketId), operation: op, maxRetries: 0 });
|
|
7182
|
+
} catch (e) {
|
|
7183
|
+
const status = statusOf(e);
|
|
7184
|
+
if (status === 404) return { status: "moved_source_absent", sourceId, newId, correlationId, snapshot: ctx.source, facts: ctx.facts };
|
|
7185
|
+
if (status !== void 0 && status >= 400 && status < 500) {
|
|
7186
|
+
return {
|
|
7187
|
+
status: "moved_manual_delete",
|
|
7188
|
+
sourceId,
|
|
7189
|
+
newId,
|
|
7190
|
+
correlationId,
|
|
7191
|
+
snapshot: ctx.source,
|
|
7192
|
+
facts: ctx.facts,
|
|
7193
|
+
manualAction: manualFor(http, spec, ctx.source, `the vendor refused to delete the source (HTTP ${status}); the verified copy exists on the target \u2014 delete the source in the Autotask UI`)
|
|
7194
|
+
};
|
|
7195
|
+
}
|
|
7196
|
+
if (status === void 0) {
|
|
7197
|
+
throw new MoveRefusedError(`${op}: the source delete outcome is unknown (never resent) \u2014 re-read the source: absent means the move completed`, { code: "MOVE_UNKNOWN", operation: op, details: { status: "unknown", ...ids, newId } });
|
|
7198
|
+
}
|
|
7199
|
+
return rollback("MOVE_SOURCE_DELETE_FAILED", `the source delete failed (HTTP ${status})`);
|
|
7200
|
+
}
|
|
7201
|
+
return { status: "moved", sourceId, newId, correlationId, snapshot: ctx.source, facts: ctx.facts };
|
|
7202
|
+
}
|
|
7203
|
+
function uiLinkFor(http, entity, id) {
|
|
7204
|
+
return buildUiLink({ entity, id, baseUrl: http.baseUrl }) ?? null;
|
|
7205
|
+
}
|
|
7206
|
+
|
|
6896
7207
|
// src/resources/special/ticket_charges.ts
|
|
6897
|
-
var
|
|
7208
|
+
var OP3 = "ticket_charges.moveToTicket";
|
|
6898
7209
|
var COPY = ["name", "description", "chargeType", "billingCodeID", "productID", "unitQuantity", "unitCost", "unitPrice", "datePurchased", "isBillableToCompany", "notes", "purchaseOrderNumber", "internalPurchaseOrderNumber"];
|
|
6899
7210
|
var CONTRACT_SCOPED = ["contractServiceID", "contractServiceBundleID"];
|
|
6900
7211
|
async function readCharge(http, id, retries) {
|
|
6901
|
-
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }],
|
|
7212
|
+
const rows = await queryRows(http, "TicketCharges", [{ field: "id", op: "eq", value: id }], OP3, retries);
|
|
6902
7213
|
return rows[0] ?? null;
|
|
6903
7214
|
}
|
|
6904
7215
|
async function checkSource({ http, source, sourceId, sourceTicketId, toTicketId }) {
|
|
6905
7216
|
if (source.isBilled === true) {
|
|
6906
|
-
throw refuse("MOVE_BLOCKED_BILLED", `${
|
|
7217
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP3}: ticket charge ${sourceId} is billed \u2014 move it in the Autotask UI`, { sourceId, toTicketId }, OP3, {
|
|
6907
7218
|
manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "tickets", sourceTicketId), reason: "billed charges are not moved by the API (the ticket link opens its charges)" }
|
|
6908
7219
|
});
|
|
6909
7220
|
}
|
|
6910
7221
|
}
|
|
6911
7222
|
var SPEC = {
|
|
6912
|
-
operation:
|
|
7223
|
+
operation: OP3,
|
|
6913
7224
|
entity: "ticket_charge",
|
|
6914
7225
|
readSource: readCharge,
|
|
6915
7226
|
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
@@ -6940,8 +7251,8 @@ var SPEC = {
|
|
|
6940
7251
|
// charges have no ExecuteCommand code: the parent ticket is the closest clickable target
|
|
6941
7252
|
manualLink: (http, source) => typeof source.ticketID === "number" ? uiLinkFor(http, "tickets", source.ticketID) : null
|
|
6942
7253
|
};
|
|
6943
|
-
function moveToTicket(
|
|
6944
|
-
return runMove(http, SPEC, sourceId, toTicketId, opts);
|
|
7254
|
+
function moveToTicket(resource, sourceId, toTicketId, opts) {
|
|
7255
|
+
return runMove(resource.http, SPEC, sourceId, toTicketId, opts);
|
|
6945
7256
|
}
|
|
6946
7257
|
|
|
6947
7258
|
// src/resources/ticket_charges.ts
|
|
@@ -7015,7 +7326,7 @@ var TicketChargesResource = class extends BaseResource {
|
|
|
7015
7326
|
}
|
|
7016
7327
|
/** Node-parity special op (hand-written in src/resources/special/ticket_charges.ts). */
|
|
7017
7328
|
moveToTicket(sourceId, toTicketId, opts = {}) {
|
|
7018
|
-
return moveToTicket(this, sourceId, toTicketId, opts
|
|
7329
|
+
return moveToTicket(this, sourceId, toTicketId, opts);
|
|
7019
7330
|
}
|
|
7020
7331
|
};
|
|
7021
7332
|
|
|
@@ -7438,765 +7749,246 @@ var TicketSecondaryResourcesResource = class extends BaseResource {
|
|
|
7438
7749
|
}
|
|
7439
7750
|
};
|
|
7440
7751
|
|
|
7441
|
-
// src/resources/special/tickets/
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7752
|
+
// src/resources/special/tickets/errors.ts
|
|
7753
|
+
var StatusUnknownError = class extends AutotaskError {
|
|
7754
|
+
/** The live picklist size (the message names it; the values + labels are via operations.fieldCatalog with labels: true). */
|
|
7755
|
+
picklistCount;
|
|
7756
|
+
constructor(message, options = {}) {
|
|
7757
|
+
super(message, {
|
|
7758
|
+
code: "STATUS_UNKNOWN",
|
|
7759
|
+
category: "validation",
|
|
7760
|
+
suggestedAction: `Pass one of the tenant's live status values (discover the current values + labels with operations.fieldCatalog({ resources: ["tickets"], fields: ["status"], labels: true })).`,
|
|
7761
|
+
...options
|
|
7762
|
+
});
|
|
7763
|
+
this.name = "StatusUnknownError";
|
|
7764
|
+
this.picklistCount = options.picklistCount ?? 0;
|
|
7451
7765
|
}
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
if (full.length <= limit) return { value: full, truncated: false, length: full.length };
|
|
7468
|
-
return { value: `${full.slice(0, limit)}\u2026`, truncated: true, length: full.length };
|
|
7469
|
-
}
|
|
7766
|
+
};
|
|
7767
|
+
var StatusFieldsRequiredError = class extends AutotaskError {
|
|
7768
|
+
vendorFields;
|
|
7769
|
+
currentStatus;
|
|
7770
|
+
targetStatus;
|
|
7771
|
+
currentValues;
|
|
7772
|
+
constructor(message, details, options = {}) {
|
|
7773
|
+
super(message, { code: "STATUS_FIELDS_REQUIRED", category: "validation", suggestedAction: "Pass the required field value(s) in the `fields` argument and retry (the node never fills a field without a proven-safe default).", ...options });
|
|
7774
|
+
this.name = "StatusFieldsRequiredError";
|
|
7775
|
+
this.vendorFields = details.vendorFields;
|
|
7776
|
+
this.currentStatus = details.currentStatus;
|
|
7777
|
+
this.targetStatus = details.targetStatus;
|
|
7778
|
+
this.currentValues = details.currentValues;
|
|
7779
|
+
}
|
|
7780
|
+
};
|
|
7470
7781
|
|
|
7471
|
-
// src/resources/special/tickets/
|
|
7472
|
-
|
|
7473
|
-
|
|
7782
|
+
// src/resources/special/tickets/writeSupport.ts
|
|
7783
|
+
var OP_STATUS = "tickets.changeStatus";
|
|
7784
|
+
var MAX_DISCOVERY_MERGES = 3;
|
|
7785
|
+
function isPositiveInt(v) {
|
|
7786
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
7474
7787
|
}
|
|
7475
|
-
function
|
|
7476
|
-
return
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7788
|
+
function namedVendorFields(err) {
|
|
7789
|
+
if (!isAutotaskError(err) || err.vendorError === void 0) return [];
|
|
7790
|
+
const entries = parseVendorErrors(err.vendorError) ?? [];
|
|
7791
|
+
const out = /* @__PURE__ */ new Set();
|
|
7792
|
+
for (const e of entries) if (e.field !== void 0) out.add(e.field);
|
|
7793
|
+
return [...out];
|
|
7481
7794
|
}
|
|
7482
|
-
function
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
);
|
|
7795
|
+
async function fetchItem(http, path, operation, readOpts) {
|
|
7796
|
+
const res = await http.request({ method: "GET", path, operation, maxRetries: readOpts?.maxRetries });
|
|
7797
|
+
return unwrapItemOrNull(res);
|
|
7486
7798
|
}
|
|
7487
|
-
function
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
const parent = entry === void 0 ? null : entry.parent;
|
|
7491
|
-
if (parent === null || parent.resource !== "tickets" || parent.segment === null || parent.field === null) return null;
|
|
7492
|
-
const parentPath = RELATIONSHIP_MAP[parent.resource]?.path;
|
|
7493
|
-
if (parentPath === null || parentPath === void 0) return null;
|
|
7494
|
-
return `/V1.0/${parentPath}/${ticketId}/${parent.segment}`;
|
|
7799
|
+
async function fetchItemUnredacted(http, path, operation, readOpts) {
|
|
7800
|
+
const res = await http.request({ method: "GET", path, operation, skipResponseRedaction: true, maxRetries: readOpts?.maxRetries });
|
|
7801
|
+
return unwrapItemOrNull(res);
|
|
7495
7802
|
}
|
|
7496
|
-
function
|
|
7497
|
-
|
|
7498
|
-
if (
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
if (
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
if (
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
const want = String(cond.value ?? "");
|
|
7511
|
-
if (cond.op === "eq" && value !== want) return false;
|
|
7512
|
-
if (cond.op === "gt" && !(value > want)) return false;
|
|
7513
|
-
if (cond.op === "lt" && !(value < want)) return false;
|
|
7514
|
-
if (cond.op !== "eq" && cond.op !== "gt" && cond.op !== "lt") return false;
|
|
7803
|
+
function unwrapItemOrNull(res) {
|
|
7804
|
+
const body = res;
|
|
7805
|
+
if (body === null || !("item" in body) || body.item === null || body.item === void 0) return null;
|
|
7806
|
+
return body.item;
|
|
7807
|
+
}
|
|
7808
|
+
function discoveryMerge(err, body, defaults) {
|
|
7809
|
+
if (!isAutotaskError(err) || err.vendorError === void 0) return null;
|
|
7810
|
+
const entries = parseVendorErrors(err.vendorError);
|
|
7811
|
+
if (entries === void 0) return null;
|
|
7812
|
+
for (const entry of entries) {
|
|
7813
|
+
const field = entry.field;
|
|
7814
|
+
if (field === void 0 || body[field] !== void 0) continue;
|
|
7815
|
+
if (defaults[field] === void 0) continue;
|
|
7816
|
+
return { field, value: defaults[field] };
|
|
7515
7817
|
}
|
|
7516
|
-
return
|
|
7818
|
+
return null;
|
|
7517
7819
|
}
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7820
|
+
|
|
7821
|
+
// src/resources/special/tickets/changeStatus.ts
|
|
7822
|
+
var STATUS_REQUIRED_FIELDS = {
|
|
7823
|
+
// <discovery output lands here — e.g. 8: { requiredFields: ['dueDateTime'], safeDefaults: { dueDateTime: '<now>' } }>
|
|
7824
|
+
};
|
|
7825
|
+
async function readLiveStatusSet(resource) {
|
|
7826
|
+
const raw = await resource.getFieldInfo();
|
|
7827
|
+
const row = raw.find((f) => f.name === "status");
|
|
7828
|
+
const values = row !== void 0 && row.isPickList === true ? row.picklistValues : void 0;
|
|
7829
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7830
|
+
if (Array.isArray(values)) {
|
|
7831
|
+
for (const pv of values) {
|
|
7832
|
+
const n = Number(pv.value);
|
|
7833
|
+
if (Number.isFinite(n)) ids.add(n);
|
|
7529
7834
|
}
|
|
7530
|
-
const body = await http.request({ method: "GET", path: target, operation });
|
|
7531
|
-
const { items: pageItems, pageDetails } = unwrapItems(body, operation);
|
|
7532
|
-
nextCursor = pageDetails.nextPageUrl;
|
|
7533
|
-
pages += 1;
|
|
7534
|
-
const window = pageItems.slice(0, Math.max(0, limit - items.length));
|
|
7535
|
-
for (const row of window) if (accept(row)) items.push(row);
|
|
7536
|
-
if (items.length >= limit) break;
|
|
7537
|
-
if (nextCursor === null || pageItems.length === 0) break;
|
|
7538
|
-
target = cursorToPath(http.baseUrl, nextCursor);
|
|
7539
7835
|
}
|
|
7540
|
-
|
|
7541
|
-
return { items, capped };
|
|
7836
|
+
return ids;
|
|
7542
7837
|
}
|
|
7543
|
-
|
|
7838
|
+
var GUARD_DEGRADED_WARNING = "the tenant's live status picklist metadata was unavailable (the local guard is degraded for this call \u2014 the W-A4 wall or a metadata failure); the vendor's own validation is the final authority on the status value";
|
|
7839
|
+
async function guardTargetStatus(resource, http, targetStatus) {
|
|
7840
|
+
let liveStatus = null;
|
|
7544
7841
|
try {
|
|
7545
|
-
const
|
|
7546
|
-
|
|
7842
|
+
const ids = await readLiveStatusSet(resource);
|
|
7843
|
+
if (ids.size > 0) liveStatus = ids;
|
|
7547
7844
|
} catch (err) {
|
|
7548
|
-
if (!
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7845
|
+
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
7846
|
+
}
|
|
7847
|
+
if (liveStatus !== null && !liveStatus.has(targetStatus)) {
|
|
7848
|
+
http.fieldCache?.invalidate("tickets:");
|
|
7849
|
+
let freshStatus = null;
|
|
7850
|
+
let reReadFailed = false;
|
|
7552
7851
|
try {
|
|
7553
|
-
const
|
|
7554
|
-
|
|
7555
|
-
} catch (
|
|
7556
|
-
|
|
7557
|
-
|
|
7852
|
+
const freshIds = await readLiveStatusSet(resource);
|
|
7853
|
+
if (freshIds.size > 0) freshStatus = freshIds;
|
|
7854
|
+
} catch (err) {
|
|
7855
|
+
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
7856
|
+
freshStatus = null;
|
|
7857
|
+
reReadFailed = true;
|
|
7858
|
+
}
|
|
7859
|
+
if (freshStatus !== null && freshStatus.has(targetStatus)) {
|
|
7860
|
+
liveStatus = freshStatus;
|
|
7861
|
+
} else {
|
|
7862
|
+
const verdict = freshStatus ?? liveStatus;
|
|
7863
|
+
const staleNote = freshStatus === null ? `; the live re-read ${reReadFailed ? "failed" : "returned no values"}, so this picklist count is from the initial read (up to one field-cache TTL old)` : "";
|
|
7864
|
+
throw new StatusUnknownError(
|
|
7865
|
+
`changeStatus: targetStatus ${targetStatus} is not in the tenant's live status picklist (live: ${verdict.size} values \u2014 the entity metadata is the oracle; the values + labels are discoverable with operations.fieldCatalog({ resources: ["tickets"], fields: ["status"], labels: true })${staleNote})`,
|
|
7866
|
+
{ operation: OP_STATUS, picklistCount: verdict.size }
|
|
7867
|
+
);
|
|
7558
7868
|
}
|
|
7559
7869
|
}
|
|
7870
|
+
return { guardDegraded: liveStatus === null };
|
|
7560
7871
|
}
|
|
7561
|
-
function
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7872
|
+
function noChangeDryRun(http, ticketId, targetStatus, guardDegraded) {
|
|
7873
|
+
return buildDryRunResult(
|
|
7874
|
+
{
|
|
7875
|
+
operation: OP_STATUS,
|
|
7876
|
+
method: "PUT",
|
|
7877
|
+
path: "/V1.0/Tickets",
|
|
7878
|
+
ids: [ticketId],
|
|
7879
|
+
checks: [
|
|
7880
|
+
{ name: "status-known", ok: true },
|
|
7881
|
+
{ name: "merge-read", ok: true },
|
|
7882
|
+
{ name: "no-op", ok: true }
|
|
7883
|
+
],
|
|
7884
|
+
impact: { affected: 0, scope: "single-record", reversible: true },
|
|
7885
|
+
warnings: [
|
|
7886
|
+
...guardDegraded ? [GUARD_DEGRADED_WARNING] : [],
|
|
7887
|
+
`NO_CHANGE: the ticket is already in the target status ${targetStatus} \u2014 the apply would be a zero-wire no-op.`
|
|
7888
|
+
],
|
|
7889
|
+
data: { reason: "NO_CHANGE", status: targetStatus }
|
|
7890
|
+
},
|
|
7891
|
+
{ redactPersonalData: http.redactPersonalData }
|
|
7579
7892
|
);
|
|
7580
|
-
const slas = rows.map((row) => {
|
|
7581
|
-
const bool = (v) => typeof v === "boolean" ? v : null;
|
|
7582
|
-
const num = (v) => typeof v === "number" ? v : null;
|
|
7583
|
-
const milestones = [
|
|
7584
|
-
{ name: "firstResponse", met: bool(row.isFirstResponseMet), elapsedHours: num(row.firstResponseElapsedHours) },
|
|
7585
|
-
{ name: "resolution", met: bool(row.isResolutionMet), elapsedHours: num(row.resolutionElapsedHours) },
|
|
7586
|
-
{ name: "resolutionPlan", met: bool(row.isResolutionPlanMet), elapsedHours: num(row.resolutionPlanElapsedHours) }
|
|
7587
|
-
];
|
|
7588
|
-
return { id: Number(row.id), name: row.serviceLevelAgreementName ?? null, milestones };
|
|
7589
|
-
});
|
|
7590
|
-
const healthy = slas.every((sla) => sla.milestones.every((m) => m.met === true));
|
|
7591
|
-
return { ticketId, slaCount: slas.length, healthy, slas, truncated: rows.length >= SLA_PAGE };
|
|
7592
7893
|
}
|
|
7593
|
-
|
|
7594
|
-
const
|
|
7595
|
-
|
|
7596
|
-
|
|
7894
|
+
function resolveMergeData(ticketId, targetStatus, previousStatus, current, fields, mapEntry) {
|
|
7895
|
+
const mergeData = { status: targetStatus };
|
|
7896
|
+
for (const [k, v] of Object.entries(fields ?? {})) {
|
|
7897
|
+
if (v !== void 0) mergeData[k] = v;
|
|
7597
7898
|
}
|
|
7598
|
-
const
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
companyID: pick(ticket.companyID),
|
|
7615
|
-
contactID: pick(ticket.contactID),
|
|
7616
|
-
assignedResourceID: pick(ticket.assignedResourceID),
|
|
7617
|
-
projectID: pick(ticket.projectID),
|
|
7618
|
-
createDate: str(ticket.createDate),
|
|
7619
|
-
dueDateTime: str(ticket.dueDateTime),
|
|
7620
|
-
ageDays
|
|
7621
|
-
};
|
|
7622
|
-
}
|
|
7623
|
-
|
|
7624
|
-
// src/resources/special/tickets/timeline.ts
|
|
7625
|
-
function validateTimelineLimits(opts) {
|
|
7626
|
-
const limit = opts.limit === void 0 ? 100 : Math.trunc(opts.limit);
|
|
7627
|
-
if (!Number.isInteger(limit) || limit < 1) throw new AutotaskConfigError(`limit must be an integer >= 1 (got ${String(opts.limit)})`);
|
|
7628
|
-
const bounded = Math.min(limit, 500);
|
|
7629
|
-
const textLimit = opts.textLimit === void 0 ? 500 : Math.trunc(opts.textLimit);
|
|
7630
|
-
if (!Number.isInteger(textLimit) || textLimit < 1) {
|
|
7631
|
-
throw new AutotaskConfigError(`textLimit must be an integer >= 1 (got ${String(opts.textLimit)})`);
|
|
7899
|
+
const filled = [];
|
|
7900
|
+
if (mapEntry !== void 0) {
|
|
7901
|
+
for (const field of mapEntry.requiredFields) {
|
|
7902
|
+
if (mergeData[field] !== void 0) continue;
|
|
7903
|
+
if (current[field] !== null && current[field] !== void 0) continue;
|
|
7904
|
+
const safe = mapEntry.safeDefaults?.[field];
|
|
7905
|
+
if (safe === void 0) {
|
|
7906
|
+
throw new StatusFieldsRequiredError(
|
|
7907
|
+
`changeStatus: status ${targetStatus} requires the field '${field}' and the live-discovered map has no proven-safe default for it \u2014 the op is safe-without-the-map (never a blind full update): supply the value in fields and retry`,
|
|
7908
|
+
{ vendorFields: [field], currentStatus: previousStatus, targetStatus, currentValues: { [field]: current[field] ?? null } },
|
|
7909
|
+
{ operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } }
|
|
7910
|
+
);
|
|
7911
|
+
}
|
|
7912
|
+
mergeData[field] = safe;
|
|
7913
|
+
filled.push(field);
|
|
7914
|
+
}
|
|
7632
7915
|
}
|
|
7633
|
-
return {
|
|
7916
|
+
return { mergeData, filled };
|
|
7634
7917
|
}
|
|
7635
|
-
function
|
|
7636
|
-
const text = truncate(typeof n.description === "string" ? n.description : null, textLimit);
|
|
7637
|
-
return {
|
|
7638
|
-
kind: "note",
|
|
7639
|
-
id: Number(n.id),
|
|
7640
|
-
date: typeof n.createDateTime === "string" ? n.createDateTime : null,
|
|
7641
|
-
text: text.value || null,
|
|
7642
|
-
title: typeof n.title === "string" ? n.title : null,
|
|
7643
|
-
hours: null,
|
|
7644
|
-
action: null,
|
|
7645
|
-
textTruncated: text.truncated
|
|
7646
|
-
};
|
|
7647
|
-
}
|
|
7648
|
-
function timeEntryEvent(t) {
|
|
7649
|
-
return {
|
|
7650
|
-
kind: "timeEntry",
|
|
7651
|
-
id: Number(t.id),
|
|
7652
|
-
date: typeof t.dateWorked === "string" ? t.dateWorked : null,
|
|
7653
|
-
text: null,
|
|
7654
|
-
title: null,
|
|
7655
|
-
hours: typeof t.hoursWorked === "number" ? t.hoursWorked : null,
|
|
7656
|
-
action: null,
|
|
7657
|
-
textTruncated: false
|
|
7658
|
-
};
|
|
7659
|
-
}
|
|
7660
|
-
function historyEvent(h, textLimit) {
|
|
7661
|
-
const text = truncate(typeof h.detail === "string" ? h.detail : null, textLimit);
|
|
7662
|
-
return {
|
|
7663
|
-
kind: "history",
|
|
7664
|
-
id: Number(h.id),
|
|
7665
|
-
date: typeof h.date === "string" ? h.date : null,
|
|
7666
|
-
text: text.value || null,
|
|
7667
|
-
title: null,
|
|
7668
|
-
hours: null,
|
|
7669
|
-
action: typeof h.action === "string" ? h.action : null,
|
|
7670
|
-
textTruncated: text.truncated
|
|
7671
|
-
};
|
|
7672
|
-
}
|
|
7673
|
-
function compareEventDates(a, b) {
|
|
7674
|
-
const da = a.date ?? "";
|
|
7675
|
-
const db = b.date ?? "";
|
|
7676
|
-
if (da === "" && db === "") return 0;
|
|
7677
|
-
if (da === "") return 1;
|
|
7678
|
-
if (db === "") return -1;
|
|
7679
|
-
return da < db ? -1 : da > db ? 1 : 0;
|
|
7680
|
-
}
|
|
7681
|
-
async function timeline(_resource, id, opts = {}, http) {
|
|
7682
|
-
const { bounded, textLimit } = validateTimelineLimits(opts);
|
|
7683
|
-
const ticketId = parseIdentifier(id, "id");
|
|
7684
|
-
await fetchTicket(http, ticketId, "tickets.timeline");
|
|
7685
|
-
const dateFilters = (field) => {
|
|
7686
|
-
const f = [{ field: "ticketID", op: "eq", value: ticketId }];
|
|
7687
|
-
if (opts.since !== void 0) f.push({ field, op: "gt", value: opts.since });
|
|
7688
|
-
if (opts.until !== void 0) f.push({ field, op: "lt", value: opts.until });
|
|
7689
|
-
return f;
|
|
7690
|
-
};
|
|
7691
|
-
const events = [];
|
|
7692
|
-
const cappedSources = [];
|
|
7693
|
-
const notes = await ticketChildRead(http, { globalPath: "/V1.0/TicketNotes/query", resourceKey: "ticket_notes", filter: dateFilters("createDateTime"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
7694
|
-
if (notes.capped) cappedSources.push("notes");
|
|
7695
|
-
for (const n of notes.items) events.push(noteEvent(n, textLimit));
|
|
7696
|
-
const entries = await ticketChildRead(http, { globalPath: "/V1.0/TimeEntries/query", resourceKey: "time_entries", filter: dateFilters("dateWorked"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
7697
|
-
if (entries.capped) cappedSources.push("timeEntries");
|
|
7698
|
-
for (const t of entries.items) events.push(timeEntryEvent(t));
|
|
7699
|
-
let historiesRead;
|
|
7700
|
-
if (opts.includeHistories) {
|
|
7701
|
-
historiesRead = await ticketChildRead(http, { globalPath: "/V1.0/TicketHistory/query", resourceKey: "ticket_history", filter: dateFilters("date"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
7702
|
-
if (historiesRead.capped) cappedSources.push("histories");
|
|
7703
|
-
for (const h of historiesRead.items) events.push(historyEvent(h, textLimit));
|
|
7704
|
-
}
|
|
7705
|
-
events.sort(compareEventDates);
|
|
7706
|
-
const capped = events.slice(0, bounded);
|
|
7707
|
-
const result = {
|
|
7708
|
-
ticketId,
|
|
7709
|
-
count: capped.length,
|
|
7710
|
-
events: capped,
|
|
7711
|
-
truncated: cappedSources.length > 0 || events.length > bounded,
|
|
7712
|
-
cappedSources
|
|
7713
|
-
};
|
|
7714
|
-
if (notes.status !== "ok" || entries.status !== "ok" || historiesRead !== void 0 && historiesRead.status !== "ok") {
|
|
7715
|
-
const routes = { notes: notes.status, timeEntries: entries.status };
|
|
7716
|
-
if (historiesRead !== void 0) routes.histories = historiesRead.status;
|
|
7717
|
-
result.sourceRoutes = routes;
|
|
7718
|
-
}
|
|
7719
|
-
return result;
|
|
7720
|
-
}
|
|
7721
|
-
|
|
7722
|
-
// src/resources/special/tickets/errors.ts
|
|
7723
|
-
var QueueRequiredError = class extends AutotaskError {
|
|
7724
|
-
constructor(message, options = {}) {
|
|
7725
|
-
super(message, {
|
|
7726
|
-
code: "QUEUE_REQUIRED",
|
|
7727
|
-
category: "validation",
|
|
7728
|
-
suggestedAction: "pass params.queueID (discover via autotask_describe(tickets.create) or a known queue id)",
|
|
7729
|
-
...options
|
|
7730
|
-
});
|
|
7731
|
-
this.name = "QueueRequiredError";
|
|
7732
|
-
}
|
|
7733
|
-
};
|
|
7734
|
-
var TicketResourceInvalidError = class extends AutotaskError {
|
|
7735
|
-
constructor(message, options = {}) {
|
|
7736
|
-
super(message, { code: "RESOURCE_INVALID", category: "validation", ...options });
|
|
7737
|
-
this.name = "TicketResourceInvalidError";
|
|
7738
|
-
}
|
|
7739
|
-
};
|
|
7740
|
-
var TicketCreateValidationFailedError = class extends AutotaskError {
|
|
7741
|
-
/** The vendor-named fields (empty when the 500 body named nothing parseable). */
|
|
7742
|
-
vendorFields;
|
|
7743
|
-
constructor(message, vendorFields, options = {}) {
|
|
7744
|
-
super(message, { code: "VALIDATION_FAILED", category: "validation", ...options });
|
|
7745
|
-
this.name = "TicketCreateValidationFailedError";
|
|
7746
|
-
this.vendorFields = vendorFields;
|
|
7747
|
-
}
|
|
7748
|
-
};
|
|
7749
|
-
var StatusUnknownError = class extends AutotaskError {
|
|
7750
|
-
/** The live picklist size (the message names it; the values + labels are via operations.fieldCatalog with labels: true). */
|
|
7751
|
-
picklistCount;
|
|
7752
|
-
constructor(message, options = {}) {
|
|
7753
|
-
super(message, {
|
|
7754
|
-
code: "STATUS_UNKNOWN",
|
|
7755
|
-
category: "validation",
|
|
7756
|
-
suggestedAction: `Pass one of the tenant's live status values (discover the current values + labels with operations.fieldCatalog({ resources: ["tickets"], fields: ["status"], labels: true })).`,
|
|
7757
|
-
...options
|
|
7758
|
-
});
|
|
7759
|
-
this.name = "StatusUnknownError";
|
|
7760
|
-
this.picklistCount = options.picklistCount ?? 0;
|
|
7761
|
-
}
|
|
7762
|
-
};
|
|
7763
|
-
var StatusFieldsRequiredError = class extends AutotaskError {
|
|
7764
|
-
vendorFields;
|
|
7765
|
-
currentStatus;
|
|
7766
|
-
targetStatus;
|
|
7767
|
-
currentValues;
|
|
7768
|
-
constructor(message, details, options = {}) {
|
|
7769
|
-
super(message, { code: "STATUS_FIELDS_REQUIRED", category: "validation", suggestedAction: "Pass the required field value(s) in the `fields` argument and retry (the node never fills a field without a proven-safe default).", ...options });
|
|
7770
|
-
this.name = "StatusFieldsRequiredError";
|
|
7771
|
-
this.vendorFields = details.vendorFields;
|
|
7772
|
-
this.currentStatus = details.currentStatus;
|
|
7773
|
-
this.targetStatus = details.targetStatus;
|
|
7774
|
-
this.currentValues = details.currentValues;
|
|
7775
|
-
}
|
|
7776
|
-
};
|
|
7777
|
-
|
|
7778
|
-
// src/resources/special/tickets/writeSupport.ts
|
|
7779
|
-
var OP_OPEN = "tickets.openWithNoteAndAssign";
|
|
7780
|
-
var OP_STATUS = "tickets.changeStatus";
|
|
7781
|
-
var MAX_DISCOVERY_MERGES = 3;
|
|
7782
|
-
function isPositiveInt(v) {
|
|
7783
|
-
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
7784
|
-
}
|
|
7785
|
-
function namedVendorFields(err) {
|
|
7786
|
-
if (!isAutotaskError(err) || err.vendorError === void 0) return [];
|
|
7787
|
-
const entries = parseVendorErrors(err.vendorError) ?? [];
|
|
7788
|
-
const out = /* @__PURE__ */ new Set();
|
|
7789
|
-
for (const e of entries) if (e.field !== void 0) out.add(e.field);
|
|
7790
|
-
return [...out];
|
|
7791
|
-
}
|
|
7792
|
-
function vendorDetailOf(err) {
|
|
7793
|
-
return isAutotaskError(err) ? formatVendorDetail(err.vendorError) : void 0;
|
|
7794
|
-
}
|
|
7795
|
-
async function fetchItem(http, path, operation, readOpts) {
|
|
7796
|
-
const res = await http.request({ method: "GET", path, operation, maxRetries: readOpts?.maxRetries });
|
|
7797
|
-
return unwrapItemOrNull(res);
|
|
7798
|
-
}
|
|
7799
|
-
async function fetchItemUnredacted(http, path, operation, readOpts) {
|
|
7800
|
-
const res = await http.request({ method: "GET", path, operation, skipResponseRedaction: true, maxRetries: readOpts?.maxRetries });
|
|
7801
|
-
return unwrapItemOrNull(res);
|
|
7802
|
-
}
|
|
7803
|
-
function unwrapItemOrNull(res) {
|
|
7804
|
-
const body = res;
|
|
7805
|
-
if (body === null || !("item" in body) || body.item === null || body.item === void 0) return null;
|
|
7806
|
-
return body.item;
|
|
7807
|
-
}
|
|
7808
|
-
function operationId(res) {
|
|
7809
|
-
const body = res;
|
|
7810
|
-
if (body === null) return void 0;
|
|
7811
|
-
return typeof body.itemId === "number" ? body.itemId : typeof body.id === "number" ? body.id : void 0;
|
|
7812
|
-
}
|
|
7813
|
-
function discoveryMerge(err, body, defaults) {
|
|
7814
|
-
if (!isAutotaskError(err) || err.vendorError === void 0) return null;
|
|
7815
|
-
const entries = parseVendorErrors(err.vendorError);
|
|
7816
|
-
if (entries === void 0) return null;
|
|
7817
|
-
for (const entry of entries) {
|
|
7818
|
-
const field = entry.field;
|
|
7819
|
-
if (field === void 0 || body[field] !== void 0) continue;
|
|
7820
|
-
if (defaults[field] === void 0) continue;
|
|
7821
|
-
return { field, value: defaults[field] };
|
|
7822
|
-
}
|
|
7823
|
-
return null;
|
|
7824
|
-
}
|
|
7825
|
-
function stepErrorText(code, err) {
|
|
7826
|
-
const detail = vendorDetailOf(err);
|
|
7827
|
-
const base = isAutotaskError(err) ? err.message : String(err);
|
|
7828
|
-
return detail ? `${code}: ${detail}` : `${code}: ${base}`;
|
|
7829
|
-
}
|
|
7830
|
-
|
|
7831
|
-
// src/resources/special/tickets/openWithNoteAndAssign.ts
|
|
7832
|
-
var DEFAULT_TICKET_PRIORITY = 3;
|
|
7833
|
-
var DEFAULT_TICKET_STATUS = 1;
|
|
7834
|
-
var DEFAULT_NOTE_TYPE = 1;
|
|
7835
|
-
var DEFAULT_PUBLISH = 1;
|
|
7836
|
-
var PUBLISH_VALUES = /* @__PURE__ */ new Set([1, 2, 4]);
|
|
7837
|
-
var SAFE_TICKET_CREATE_DEFAULTS = {};
|
|
7838
|
-
function validateOpenInput(params, note, assign) {
|
|
7839
|
-
const companyID = parseIdentifier(params?.companyID, "params.companyID");
|
|
7840
|
-
const title = params?.title;
|
|
7841
|
-
if (typeof title !== "string" || title.trim() === "") {
|
|
7842
|
-
throw new AutotaskConfigError("openWithNoteAndAssign: params.title is required (a non-empty string)", { operation: OP_OPEN });
|
|
7843
|
-
}
|
|
7844
|
-
const priority = params.priority ?? DEFAULT_TICKET_PRIORITY;
|
|
7845
|
-
const status = params.status ?? DEFAULT_TICKET_STATUS;
|
|
7846
|
-
if (!isPositiveInt(priority)) {
|
|
7847
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: params.priority must be a positive integer (got ${String(params.priority)})`, { operation: OP_OPEN });
|
|
7848
|
-
}
|
|
7849
|
-
if (!isPositiveInt(status)) {
|
|
7850
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: params.status must be a positive integer (got ${String(params.status)})`, { operation: OP_OPEN });
|
|
7851
|
-
}
|
|
7852
|
-
if (params.queueID !== void 0 && !isPositiveInt(params.queueID)) {
|
|
7853
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: params.queueID must be a positive integer (got ${String(params.queueID)})`, { operation: OP_OPEN });
|
|
7854
|
-
}
|
|
7855
|
-
if (note !== void 0) {
|
|
7856
|
-
if (typeof note.description !== "string" || note.description.trim() === "") {
|
|
7857
|
-
throw new AutotaskConfigError("openWithNoteAndAssign: note.description is required (a non-empty string)", { operation: OP_OPEN });
|
|
7858
|
-
}
|
|
7859
|
-
if (typeof note.title !== "string" || note.title.trim() === "") {
|
|
7860
|
-
throw new AutotaskConfigError("openWithNoteAndAssign: note.title is required (a non-empty string) \u2014 the ww29 scoped note create 500s without it (S12-A live-verified: Missing Required Field: title; the S11-B proven note body is { title, description, noteType: 1, publish: 1 })", { operation: OP_OPEN });
|
|
7861
|
-
}
|
|
7862
|
-
if (note.noteType !== void 0 && !isPositiveInt(note.noteType)) {
|
|
7863
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: note.noteType must be a positive integer (got ${String(note.noteType)})`, { operation: OP_OPEN });
|
|
7864
|
-
}
|
|
7865
|
-
if (note.publish !== void 0 && !PUBLISH_VALUES.has(note.publish)) {
|
|
7866
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: note.publish must be an integer picklist value 1/2/4 (got ${String(note.publish)} \u2014 NOT a boolean)`, { operation: OP_OPEN });
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
|
-
const assignee = assign === void 0 ? void 0 : parseIdentifier(assign.resourceID, "assign.resourceID");
|
|
7870
|
-
if (assign !== void 0 && assignee !== void 0 && (typeof assignee === "number" ? assignee < 1 : false)) {
|
|
7871
|
-
throw new AutotaskConfigError(`openWithNoteAndAssign: assign.resourceID must be a positive integer (got ${String(assignee)})`, { operation: OP_OPEN });
|
|
7872
|
-
}
|
|
7873
|
-
const roleID = assign?.roleID;
|
|
7874
|
-
if (assign !== void 0 && !isPositiveInt(roleID)) {
|
|
7875
|
-
throw new AutotaskConfigError("openWithNoteAndAssign: assign.roleID is required (a positive integer) \u2014 the ww29 vendor requires BOTH assignedResourceID and assignedResourceRoleID when assigning a resource (S12-A live-verified: Data violation: When assigning a Resource, you must assign both a assignedResourceID and assignedResourceRoleID)", { operation: OP_OPEN });
|
|
7876
|
-
}
|
|
7877
|
-
return { companyID, title, priority, status, assignee, roleID };
|
|
7878
|
-
}
|
|
7879
|
-
async function precheckAssignee(http, assignee, opts) {
|
|
7880
|
-
const rid = assignee;
|
|
7881
|
-
const rec = await fetchItem(http, `/V1.0/Resources/${rid}`, OP_OPEN, { maxRetries: opts.maxRetries });
|
|
7882
|
-
if (rec === null) {
|
|
7883
|
-
throw new TicketResourceInvalidError(
|
|
7884
|
-
`openWithNoteAndAssign: assignee resource ${assignee} was not found (resources.get returned item: null \u2014 the resources table is blind to the API user; get is the authority)`,
|
|
7885
|
-
{ resourceIds: [rid], details: { idRefs: [{ entity: "resource", id: rid }] }, operation: OP_OPEN, category: "not_found", suggestedAction: "Resolve the resource id (a ticket assignedResourceID via tickets.fullContext) and retry." }
|
|
7886
|
-
);
|
|
7887
|
-
}
|
|
7888
|
-
if (rec.isActive !== true) {
|
|
7889
|
-
throw new TicketResourceInvalidError(
|
|
7890
|
-
`openWithNoteAndAssign: assignee resource ${assignee} is not active (isActive !== true) \u2014 the vendor would reject the assignment`,
|
|
7891
|
-
{ resourceIds: [rid], details: { idRefs: [{ entity: "resource", id: rid }] }, operation: OP_OPEN, suggestedAction: "Activate the resource or assign an active one." }
|
|
7892
|
-
);
|
|
7893
|
-
}
|
|
7894
|
-
}
|
|
7895
|
-
function openDryRun(http, v, params, assign, body, noteBody) {
|
|
7896
|
-
const { companyID, assignee, roleID } = v;
|
|
7918
|
+
function mergeDryRun(http, ticketId, targetStatus, guardDegraded, filled, mapEntry, mergeData) {
|
|
7897
7919
|
const warnings = [];
|
|
7898
|
-
if (
|
|
7899
|
-
|
|
7920
|
+
if (guardDegraded) warnings.push(GUARD_DEGRADED_WARNING);
|
|
7921
|
+
if (filled.length > 0) {
|
|
7922
|
+
warnings.push(`the node will fill the map-required field(s) ${filled.join(", ")} with the proven-safe defaults (the live-discovered map); the caller value always wins.`);
|
|
7900
7923
|
}
|
|
7901
|
-
if (
|
|
7902
|
-
warnings.push(`
|
|
7924
|
+
if (mapEntry === void 0) {
|
|
7925
|
+
warnings.push(`status ${targetStatus} has no entry in the live-discovered required-field map yet \u2014 if the vendor 500 names a required field without a safe default, the apply throws the typed STATUS_FIELDS_REQUIRED (the caller supplies the value; never a blind retry).`);
|
|
7903
7926
|
}
|
|
7904
7927
|
return buildDryRunResult(
|
|
7905
7928
|
{
|
|
7906
|
-
operation:
|
|
7907
|
-
method: "
|
|
7929
|
+
operation: OP_STATUS,
|
|
7930
|
+
method: "PUT",
|
|
7908
7931
|
path: "/V1.0/Tickets",
|
|
7909
|
-
ids: [
|
|
7932
|
+
ids: [ticketId],
|
|
7910
7933
|
checks: [
|
|
7911
|
-
{ name: "
|
|
7912
|
-
{ name: "
|
|
7934
|
+
{ name: "status-known", ok: true },
|
|
7935
|
+
{ name: "merge-read", ok: true },
|
|
7936
|
+
{ name: "fields-resolved", ok: true }
|
|
7913
7937
|
],
|
|
7914
|
-
impact: {
|
|
7915
|
-
affected: 1 + (noteBody !== null ? 1 : 0) + (assign !== void 0 ? 1 : 0),
|
|
7916
|
-
scope: "ticket (+ scoped note, + assignment merge)",
|
|
7917
|
-
reversible: false
|
|
7918
|
-
},
|
|
7938
|
+
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
7919
7939
|
warnings,
|
|
7920
|
-
data:
|
|
7921
|
-
ticket: body,
|
|
7922
|
-
note: noteBody,
|
|
7923
|
-
assign: assign !== void 0 ? { method: "GET+PUT", path: `/V1.0/Tickets/<newId>`, body: { assignedResourceID: assignee, assignedResourceRoleID: roleID } } : null
|
|
7924
|
-
}
|
|
7940
|
+
data: mergeData
|
|
7925
7941
|
},
|
|
7926
7942
|
{ redactPersonalData: http.redactPersonalData }
|
|
7927
7943
|
);
|
|
7928
7944
|
}
|
|
7929
|
-
async function
|
|
7945
|
+
async function putStatusWithDiscovery(http, ticketId, targetStatus, previousStatus, current, mergeData, mapEntry, filled, opts) {
|
|
7946
|
+
const safeDefaults = mapEntry?.safeDefaults ?? {};
|
|
7947
|
+
const body = mergePartialBody(current, mergeData);
|
|
7948
|
+
body.id = ticketId;
|
|
7930
7949
|
let merges = 0;
|
|
7931
7950
|
for (; ; ) {
|
|
7932
7951
|
try {
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
return created.id;
|
|
7952
|
+
await http.request({ method: "PUT", path: "/V1.0/Tickets", body, operation: OP_STATUS, maxRetries: opts.maxRetries });
|
|
7953
|
+
return merges;
|
|
7936
7954
|
} catch (err) {
|
|
7937
7955
|
const named = namedVendorFields(err);
|
|
7938
|
-
|
|
7939
|
-
const detail = vendorDetailOf(err);
|
|
7940
|
-
throw new QueueRequiredError(
|
|
7941
|
-
`openWithNoteAndAssign: the vendor rejected the ticket create naming 'queueID' (the ww29 live-required set is { title, status, priority, companyID, queueID }) and the node never guesses a queue${detail ? ` \u2014 ${detail}` : ""}`,
|
|
7942
|
-
{
|
|
7943
|
-
operation: OP_OPEN,
|
|
7944
|
-
httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
|
|
7945
|
-
vendorError: isAutotaskError(err) ? err.vendorError : void 0
|
|
7946
|
-
}
|
|
7947
|
-
);
|
|
7948
|
-
}
|
|
7949
|
-
const merge = discoveryMerge(err, body, SAFE_TICKET_CREATE_DEFAULTS);
|
|
7956
|
+
const merge = discoveryMerge(err, body, safeDefaults);
|
|
7950
7957
|
if (merge !== null && merges < MAX_DISCOVERY_MERGES) {
|
|
7951
7958
|
body[merge.field] = merge.value;
|
|
7959
|
+
if (!filled.includes(merge.field)) filled.push(merge.field);
|
|
7952
7960
|
merges += 1;
|
|
7953
7961
|
continue;
|
|
7954
7962
|
}
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
named
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7963
|
+
if (named.length > 0 && named.every((f) => safeDefaults[f] === void 0)) {
|
|
7964
|
+
const currentValues = {};
|
|
7965
|
+
for (const f of named) currentValues[f] = current[f] ?? null;
|
|
7966
|
+
throw new StatusFieldsRequiredError(
|
|
7967
|
+
`changeStatus: the vendor rejected the status change ${previousStatus ?? "?"} \u2192 ${targetStatus} naming field(s) ${named.join(", ")} that have no proven-safe default \u2014 supply the value(s) in fields and retry (no blind retry)`,
|
|
7968
|
+
{ vendorFields: named, currentStatus: previousStatus, targetStatus, currentValues },
|
|
7969
|
+
{
|
|
7970
|
+
operation: OP_STATUS,
|
|
7971
|
+
httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
|
|
7972
|
+
vendorError: isAutotaskError(err) ? err.vendorError : void 0,
|
|
7973
|
+
resourceIds: [ticketId],
|
|
7974
|
+
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
7975
|
+
}
|
|
7976
|
+
);
|
|
7977
|
+
}
|
|
7978
|
+
throw err;
|
|
7966
7979
|
}
|
|
7967
7980
|
}
|
|
7968
7981
|
}
|
|
7969
|
-
async function
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
}
|
|
7980
|
-
async function assignStep(http, ticketId, assignee, roleID, opts, steps) {
|
|
7981
|
-
let assigned = false;
|
|
7982
|
-
let ticketNumber = null;
|
|
7983
|
-
try {
|
|
7984
|
-
const current = await fetchItemUnredacted(http, `/V1.0/Tickets/${ticketId}`, OP_OPEN, { maxRetries: opts.maxRetries });
|
|
7985
|
-
if (current === null) {
|
|
7986
|
-
throw new NotFoundError(`openWithNoteAndAssign: ticket ${ticketId} vanished between the create and the assign merge read`, { operation: OP_OPEN, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } });
|
|
7987
|
-
}
|
|
7988
|
-
ticketNumber = typeof current.ticketNumber === "string" ? current.ticketNumber : null;
|
|
7989
|
-
const merged = mergePartialBody(current, { id: ticketId, assignedResourceID: assignee, assignedResourceRoleID: roleID });
|
|
7990
|
-
merged.id = ticketId;
|
|
7991
|
-
await http.request({ method: "PUT", path: "/V1.0/Tickets", body: merged, operation: OP_OPEN, maxRetries: opts.maxRetries });
|
|
7992
|
-
assigned = true;
|
|
7993
|
-
steps.push({ name: "assign", ok: true });
|
|
7994
|
-
} catch (err) {
|
|
7995
|
-
steps.push({ name: "assign", ok: false, error: stepErrorText("ASSIGN_FAILED", err) });
|
|
7996
|
-
}
|
|
7997
|
-
return { assigned, ticketNumber };
|
|
7998
|
-
}
|
|
7999
|
-
async function openWithNoteAndAssign(resource, params, note, assign, opts, http) {
|
|
8000
|
-
const validated = validateOpenInput(params, note, assign);
|
|
8001
|
-
const { companyID, title, priority, status, assignee } = validated;
|
|
8002
|
-
if (assign !== void 0 && assignee !== void 0) await precheckAssignee(http, assignee, opts);
|
|
8003
|
-
const body = { title, status, priority, companyID };
|
|
8004
|
-
if (params.queueID !== void 0) body.queueID = params.queueID;
|
|
8005
|
-
const noteBody = note === void 0 ? null : {
|
|
8006
|
-
title: note.title,
|
|
8007
|
-
// REQUIRED by the vendor (S12-A live-verified: Missing Required Field: title)
|
|
8008
|
-
description: note.description,
|
|
8009
|
-
noteType: note.noteType ?? DEFAULT_NOTE_TYPE,
|
|
8010
|
-
publish: note.publish ?? DEFAULT_PUBLISH
|
|
8011
|
-
};
|
|
8012
|
-
if (opts.dryRun === true) return openDryRun(http, validated, params, assign, body, noteBody);
|
|
8013
|
-
const steps = [];
|
|
8014
|
-
const ticketId = await createTicketWithDiscovery(resource, params, companyID, body, steps);
|
|
8015
|
-
const noteId = noteBody !== null ? await createNoteStep(http, ticketId, noteBody, opts, steps) : null;
|
|
8016
|
-
let assigned = false;
|
|
8017
|
-
let ticketNumber = null;
|
|
8018
|
-
if (assign !== void 0) ({ assigned, ticketNumber } = await assignStep(http, ticketId, assignee, validated.roleID, opts, steps));
|
|
8019
|
-
return {
|
|
8020
|
-
ticketId,
|
|
8021
|
-
ticketNumber,
|
|
8022
|
-
noteId,
|
|
8023
|
-
assigned,
|
|
8024
|
-
steps,
|
|
8025
|
-
...assign !== void 0 ? { blind: { resources: true, note: "the resources table is blind to this API user (count/query return 0) \u2014 the assignee was verified via resources.get (the authority)" } } : {}
|
|
8026
|
-
};
|
|
8027
|
-
}
|
|
8028
|
-
|
|
8029
|
-
// src/resources/special/tickets/changeStatus.ts
|
|
8030
|
-
var STATUS_REQUIRED_FIELDS = {
|
|
8031
|
-
// <discovery output lands here — e.g. 8: { requiredFields: ['dueDateTime'], safeDefaults: { dueDateTime: '<now>' } }>
|
|
8032
|
-
};
|
|
8033
|
-
async function readLiveStatusSet(resource) {
|
|
8034
|
-
const raw = await resource.getFieldInfo();
|
|
8035
|
-
const row = raw.find((f) => f.name === "status");
|
|
8036
|
-
const values = row !== void 0 && row.isPickList === true ? row.picklistValues : void 0;
|
|
8037
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8038
|
-
if (Array.isArray(values)) {
|
|
8039
|
-
for (const pv of values) {
|
|
8040
|
-
const n = Number(pv.value);
|
|
8041
|
-
if (Number.isFinite(n)) ids.add(n);
|
|
8042
|
-
}
|
|
8043
|
-
}
|
|
8044
|
-
return ids;
|
|
8045
|
-
}
|
|
8046
|
-
var GUARD_DEGRADED_WARNING = "the tenant's live status picklist metadata was unavailable (the local guard is degraded for this call \u2014 the W-A4 wall or a metadata failure); the vendor's own validation is the final authority on the status value";
|
|
8047
|
-
async function guardTargetStatus(resource, http, targetStatus) {
|
|
8048
|
-
let liveStatus = null;
|
|
8049
|
-
try {
|
|
8050
|
-
const ids = await readLiveStatusSet(resource);
|
|
8051
|
-
if (ids.size > 0) liveStatus = ids;
|
|
8052
|
-
} catch (err) {
|
|
8053
|
-
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
8054
|
-
}
|
|
8055
|
-
if (liveStatus !== null && !liveStatus.has(targetStatus)) {
|
|
8056
|
-
http.fieldCache?.invalidate("tickets:");
|
|
8057
|
-
let freshStatus = null;
|
|
8058
|
-
let reReadFailed = false;
|
|
8059
|
-
try {
|
|
8060
|
-
const freshIds = await readLiveStatusSet(resource);
|
|
8061
|
-
if (freshIds.size > 0) freshStatus = freshIds;
|
|
8062
|
-
} catch (err) {
|
|
8063
|
-
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
8064
|
-
freshStatus = null;
|
|
8065
|
-
reReadFailed = true;
|
|
8066
|
-
}
|
|
8067
|
-
if (freshStatus !== null && freshStatus.has(targetStatus)) {
|
|
8068
|
-
liveStatus = freshStatus;
|
|
8069
|
-
} else {
|
|
8070
|
-
const verdict = freshStatus ?? liveStatus;
|
|
8071
|
-
const staleNote = freshStatus === null ? `; the live re-read ${reReadFailed ? "failed" : "returned no values"}, so this picklist count is from the initial read (up to one field-cache TTL old)` : "";
|
|
8072
|
-
throw new StatusUnknownError(
|
|
8073
|
-
`changeStatus: targetStatus ${targetStatus} is not in the tenant's live status picklist (live: ${verdict.size} values \u2014 the entity metadata is the oracle; the values + labels are discoverable with operations.fieldCatalog({ resources: ["tickets"], fields: ["status"], labels: true })${staleNote})`,
|
|
8074
|
-
{ operation: OP_STATUS, picklistCount: verdict.size }
|
|
8075
|
-
);
|
|
8076
|
-
}
|
|
8077
|
-
}
|
|
8078
|
-
return { guardDegraded: liveStatus === null };
|
|
8079
|
-
}
|
|
8080
|
-
function noChangeDryRun(http, ticketId, targetStatus, guardDegraded) {
|
|
8081
|
-
return buildDryRunResult(
|
|
8082
|
-
{
|
|
8083
|
-
operation: OP_STATUS,
|
|
8084
|
-
method: "PUT",
|
|
8085
|
-
path: "/V1.0/Tickets",
|
|
8086
|
-
ids: [ticketId],
|
|
8087
|
-
checks: [
|
|
8088
|
-
{ name: "status-known", ok: true },
|
|
8089
|
-
{ name: "merge-read", ok: true },
|
|
8090
|
-
{ name: "no-op", ok: true }
|
|
8091
|
-
],
|
|
8092
|
-
impact: { affected: 0, scope: "single-record", reversible: true },
|
|
8093
|
-
warnings: [
|
|
8094
|
-
...guardDegraded ? [GUARD_DEGRADED_WARNING] : [],
|
|
8095
|
-
`NO_CHANGE: the ticket is already in the target status ${targetStatus} \u2014 the apply would be a zero-wire no-op.`
|
|
8096
|
-
],
|
|
8097
|
-
data: { reason: "NO_CHANGE", status: targetStatus }
|
|
8098
|
-
},
|
|
8099
|
-
{ redactPersonalData: http.redactPersonalData }
|
|
8100
|
-
);
|
|
8101
|
-
}
|
|
8102
|
-
function resolveMergeData(ticketId, targetStatus, previousStatus, current, fields, mapEntry) {
|
|
8103
|
-
const mergeData = { status: targetStatus };
|
|
8104
|
-
for (const [k, v] of Object.entries(fields ?? {})) {
|
|
8105
|
-
if (v !== void 0) mergeData[k] = v;
|
|
8106
|
-
}
|
|
8107
|
-
const filled = [];
|
|
8108
|
-
if (mapEntry !== void 0) {
|
|
8109
|
-
for (const field of mapEntry.requiredFields) {
|
|
8110
|
-
if (mergeData[field] !== void 0) continue;
|
|
8111
|
-
if (current[field] !== null && current[field] !== void 0) continue;
|
|
8112
|
-
const safe = mapEntry.safeDefaults?.[field];
|
|
8113
|
-
if (safe === void 0) {
|
|
8114
|
-
throw new StatusFieldsRequiredError(
|
|
8115
|
-
`changeStatus: status ${targetStatus} requires the field '${field}' and the live-discovered map has no proven-safe default for it \u2014 the op is safe-without-the-map (never a blind full update): supply the value in fields and retry`,
|
|
8116
|
-
{ vendorFields: [field], currentStatus: previousStatus, targetStatus, currentValues: { [field]: current[field] ?? null } },
|
|
8117
|
-
{ operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } }
|
|
8118
|
-
);
|
|
8119
|
-
}
|
|
8120
|
-
mergeData[field] = safe;
|
|
8121
|
-
filled.push(field);
|
|
8122
|
-
}
|
|
8123
|
-
}
|
|
8124
|
-
return { mergeData, filled };
|
|
8125
|
-
}
|
|
8126
|
-
function mergeDryRun(http, ticketId, targetStatus, guardDegraded, filled, mapEntry, mergeData) {
|
|
8127
|
-
const warnings = [];
|
|
8128
|
-
if (guardDegraded) warnings.push(GUARD_DEGRADED_WARNING);
|
|
8129
|
-
if (filled.length > 0) {
|
|
8130
|
-
warnings.push(`the node will fill the map-required field(s) ${filled.join(", ")} with the proven-safe defaults (the live-discovered map); the caller value always wins.`);
|
|
8131
|
-
}
|
|
8132
|
-
if (mapEntry === void 0) {
|
|
8133
|
-
warnings.push(`status ${targetStatus} has no entry in the live-discovered required-field map yet \u2014 if the vendor 500 names a required field without a safe default, the apply throws the typed STATUS_FIELDS_REQUIRED (the caller supplies the value; never a blind retry).`);
|
|
8134
|
-
}
|
|
8135
|
-
return buildDryRunResult(
|
|
8136
|
-
{
|
|
8137
|
-
operation: OP_STATUS,
|
|
8138
|
-
method: "PUT",
|
|
8139
|
-
path: "/V1.0/Tickets",
|
|
8140
|
-
ids: [ticketId],
|
|
8141
|
-
checks: [
|
|
8142
|
-
{ name: "status-known", ok: true },
|
|
8143
|
-
{ name: "merge-read", ok: true },
|
|
8144
|
-
{ name: "fields-resolved", ok: true }
|
|
8145
|
-
],
|
|
8146
|
-
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
8147
|
-
warnings,
|
|
8148
|
-
data: mergeData
|
|
8149
|
-
},
|
|
8150
|
-
{ redactPersonalData: http.redactPersonalData }
|
|
8151
|
-
);
|
|
8152
|
-
}
|
|
8153
|
-
async function putStatusWithDiscovery(http, ticketId, targetStatus, previousStatus, current, mergeData, mapEntry, filled, opts) {
|
|
8154
|
-
const safeDefaults = mapEntry?.safeDefaults ?? {};
|
|
8155
|
-
const body = mergePartialBody(current, mergeData);
|
|
8156
|
-
body.id = ticketId;
|
|
8157
|
-
let merges = 0;
|
|
8158
|
-
for (; ; ) {
|
|
8159
|
-
try {
|
|
8160
|
-
await http.request({ method: "PUT", path: "/V1.0/Tickets", body, operation: OP_STATUS, maxRetries: opts.maxRetries });
|
|
8161
|
-
return merges;
|
|
8162
|
-
} catch (err) {
|
|
8163
|
-
const named = namedVendorFields(err);
|
|
8164
|
-
const merge = discoveryMerge(err, body, safeDefaults);
|
|
8165
|
-
if (merge !== null && merges < MAX_DISCOVERY_MERGES) {
|
|
8166
|
-
body[merge.field] = merge.value;
|
|
8167
|
-
if (!filled.includes(merge.field)) filled.push(merge.field);
|
|
8168
|
-
merges += 1;
|
|
8169
|
-
continue;
|
|
8170
|
-
}
|
|
8171
|
-
if (named.length > 0 && named.every((f) => safeDefaults[f] === void 0)) {
|
|
8172
|
-
const currentValues = {};
|
|
8173
|
-
for (const f of named) currentValues[f] = current[f] ?? null;
|
|
8174
|
-
throw new StatusFieldsRequiredError(
|
|
8175
|
-
`changeStatus: the vendor rejected the status change ${previousStatus ?? "?"} \u2192 ${targetStatus} naming field(s) ${named.join(", ")} that have no proven-safe default \u2014 supply the value(s) in fields and retry (no blind retry)`,
|
|
8176
|
-
{ vendorFields: named, currentStatus: previousStatus, targetStatus, currentValues },
|
|
8177
|
-
{
|
|
8178
|
-
operation: OP_STATUS,
|
|
8179
|
-
httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
|
|
8180
|
-
vendorError: isAutotaskError(err) ? err.vendorError : void 0,
|
|
8181
|
-
resourceIds: [ticketId],
|
|
8182
|
-
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
8183
|
-
}
|
|
8184
|
-
);
|
|
8185
|
-
}
|
|
8186
|
-
throw err;
|
|
8187
|
-
}
|
|
8188
|
-
}
|
|
8189
|
-
}
|
|
8190
|
-
async function verifyStatusApplied(http, ticketId, targetStatus, opts) {
|
|
8191
|
-
const verify = await fetchItem(http, `/V1.0/Tickets/${ticketId}`, OP_STATUS, { maxRetries: opts.maxRetries });
|
|
8192
|
-
if (verify === null) {
|
|
8193
|
-
throw new AutotaskError(`changeStatus: the post-PUT read-back found no ticket ${ticketId} (the PUT 200ed but the record is gone)`, {
|
|
8194
|
-
code: "INVALID_RESPONSE",
|
|
8195
|
-
category: "server",
|
|
8196
|
-
operation: OP_STATUS,
|
|
8197
|
-
resourceIds: [ticketId],
|
|
8198
|
-
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
8199
|
-
});
|
|
7982
|
+
async function verifyStatusApplied(http, ticketId, targetStatus, opts) {
|
|
7983
|
+
const verify = await fetchItem(http, `/V1.0/Tickets/${ticketId}`, OP_STATUS, { maxRetries: opts.maxRetries });
|
|
7984
|
+
if (verify === null) {
|
|
7985
|
+
throw new AutotaskError(`changeStatus: the post-PUT read-back found no ticket ${ticketId} (the PUT 200ed but the record is gone)`, {
|
|
7986
|
+
code: "INVALID_RESPONSE",
|
|
7987
|
+
category: "server",
|
|
7988
|
+
operation: OP_STATUS,
|
|
7989
|
+
resourceIds: [ticketId],
|
|
7990
|
+
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
7991
|
+
});
|
|
8200
7992
|
}
|
|
8201
7993
|
const finalStatus = typeof verify.status === "number" ? verify.status : null;
|
|
8202
7994
|
if (finalStatus !== targetStatus) {
|
|
@@ -8214,7 +8006,8 @@ async function verifyStatusApplied(http, ticketId, targetStatus, opts) {
|
|
|
8214
8006
|
);
|
|
8215
8007
|
}
|
|
8216
8008
|
}
|
|
8217
|
-
async function changeStatus(resource, id, targetStatus, fields, opts
|
|
8009
|
+
async function changeStatus(resource, id, targetStatus, fields, opts) {
|
|
8010
|
+
const http = resource.http;
|
|
8218
8011
|
const ticketId = parseIdentifier(id, "id");
|
|
8219
8012
|
if (!isPositiveInt(targetStatus)) {
|
|
8220
8013
|
throw new AutotaskConfigError(`changeStatus: targetStatus must be a positive integer (got ${String(targetStatus)})`, { operation: OP_STATUS });
|
|
@@ -8240,199 +8033,78 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
8240
8033
|
return { ticketId, previousStatus, status: targetStatus, changed: true, filledFields: filled, discoveryMerges: merges };
|
|
8241
8034
|
}
|
|
8242
8035
|
|
|
8243
|
-
// src/resources/special/tickets/
|
|
8244
|
-
var
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8036
|
+
// src/resources/special/tickets/forResource.ts
|
|
8037
|
+
var OP4 = "tickets.forResource";
|
|
8038
|
+
var TICKET_CHUNK = 100;
|
|
8039
|
+
var LIMIT_DEFAULT = 100;
|
|
8040
|
+
var LIMIT_MAX = 500;
|
|
8041
|
+
var LINK_READ_CAP = 500;
|
|
8042
|
+
var COUNT_LINK_PAGES = 20;
|
|
8043
|
+
var TICKET_FIELDS = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
8044
|
+
function positiveId(value, name) {
|
|
8045
|
+
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
8046
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
8047
|
+
throw new AutotaskConfigError(`${OP4}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP4 });
|
|
8249
8048
|
}
|
|
8250
8049
|
return n;
|
|
8251
8050
|
}
|
|
8252
|
-
function
|
|
8253
|
-
const
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8051
|
+
async function ticketPage(http, filter, maxRecords, fields) {
|
|
8052
|
+
const res = await http.request({
|
|
8053
|
+
method: "POST",
|
|
8054
|
+
idempotent: true,
|
|
8055
|
+
path: "/V1.0/Tickets/query",
|
|
8056
|
+
body: { MaxRecords: maxRecords, filter, IncludeFields: fields },
|
|
8057
|
+
operation: OP4
|
|
8058
|
+
});
|
|
8059
|
+
const page = unwrapItems(res, OP4);
|
|
8060
|
+
return { rows: page.items, more: page.pageDetails.nextPageUrl !== null };
|
|
8259
8061
|
}
|
|
8260
|
-
async function
|
|
8261
|
-
const
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8062
|
+
async function readLinkIds(http, resourceID, maxPages = 1) {
|
|
8063
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8064
|
+
let cursor = null;
|
|
8065
|
+
for (let pages = 0; pages < maxPages; pages++) {
|
|
8066
|
+
const res = await http.request({
|
|
8067
|
+
method: "POST",
|
|
8068
|
+
idempotent: true,
|
|
8069
|
+
path: cursor === null ? "/V1.0/TicketSecondaryResources/query" : cursorToPath(http.baseUrl, cursor),
|
|
8070
|
+
body: { MaxRecords: LINK_READ_CAP, filter: [{ field: "resourceID", op: "eq", value: resourceID }], IncludeFields: ["ticketID"] },
|
|
8071
|
+
operation: OP4
|
|
8072
|
+
});
|
|
8073
|
+
const page = unwrapItems(res, OP4);
|
|
8074
|
+
for (const row of page.items) if (typeof row.ticketID === "number") ids.add(row.ticketID);
|
|
8075
|
+
cursor = page.pageDetails.nextPageUrl;
|
|
8076
|
+
if (cursor === null || page.items.length === 0) return { ids: [...ids], capped: false };
|
|
8272
8077
|
}
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
const
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
addNameSibling(ticketRow, "contractID", (v) => names.recordName("contract", v));
|
|
8283
|
-
addNameSibling(ticketRow, "billingCodeID", (v) => names.recordName("billingCode", v));
|
|
8284
|
-
addNameSibling(ticketRow, "assignedResourceRoleID", (v) => names.recordName("role", v));
|
|
8285
|
-
for (const f of TICKET_LABEL_FIELDS) addLabelSibling(ticketRow, f, names);
|
|
8286
|
-
result.ticket = ticketRow;
|
|
8287
|
-
result.timeEntries.items = entriesPart.items.map((e) => {
|
|
8288
|
-
const row = { ...e };
|
|
8289
|
-
addNameSibling(row, "resourceID", (v) => names.resourceName(v));
|
|
8290
|
-
addNameSibling(row, "roleID", (v) => names.recordName("role", v));
|
|
8291
|
-
addNameSibling(row, "billingCodeID", (v) => names.recordName("billingCode", v));
|
|
8292
|
-
addNameSibling(row, "contractID", (v) => names.recordName("contract", v));
|
|
8293
|
-
return row;
|
|
8294
|
-
});
|
|
8295
|
-
result.names = names.section;
|
|
8296
|
-
result.lineOfBusiness = names.lineOfBusiness ?? {
|
|
8297
|
-
association: null,
|
|
8298
|
-
organizationalLevel2: null,
|
|
8299
|
-
organizationalLevel1: null,
|
|
8300
|
-
markers: { association: "missing", organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
8301
|
-
};
|
|
8302
|
-
result.truncated = result.truncated || names.section.truncated;
|
|
8303
|
-
}
|
|
8304
|
-
async function fullContext(resource, id, opts = {}, http) {
|
|
8305
|
-
const noteLimit = partLimit(opts.noteLimit, "noteLimit");
|
|
8306
|
-
const timeEntryLimit = partLimit(opts.timeEntryLimit, "timeEntryLimit");
|
|
8307
|
-
const textLimit = partLimit(opts.textLimit, "textLimit");
|
|
8308
|
-
const ticketId = parseIdentifier(id, "id");
|
|
8309
|
-
const ctx = await resource.getContext(ticketId);
|
|
8310
|
-
const op = "tickets.fullContext";
|
|
8311
|
-
const ticketFilter = [{ field: "ticketID", op: "eq", value: ticketId }];
|
|
8312
|
-
const notes = await ticketChildRead(http, { globalPath: "/V1.0/TicketNotes/query", resourceKey: "ticket_notes", filter: ticketFilter, limit: noteLimit, operation: op }, ticketId);
|
|
8313
|
-
const entries = await ticketChildRead(http, { globalPath: "/V1.0/TimeEntries/query", resourceKey: "time_entries", filter: ticketFilter, limit: timeEntryLimit, operation: op }, ticketId);
|
|
8314
|
-
const notesRows = notes.items.map((n) => servedTextRecord(n, "description", textLimit));
|
|
8315
|
-
const notesPart = { items: notesRows, capped: notes.capped };
|
|
8316
|
-
applyRouteStatus(notesPart, notes);
|
|
8317
|
-
const entriesPart = { items: entries.items, capped: entries.capped };
|
|
8318
|
-
applyRouteStatus(entriesPart, entries);
|
|
8319
|
-
const notesTextTruncated = notesRows.some((n) => n.descriptionTruncated === true);
|
|
8320
|
-
let attachments = { ids: [], names: [], capped: false };
|
|
8321
|
-
let attachmentsStatus = "ok";
|
|
8322
|
-
if (opts.includeAttachments !== false) {
|
|
8323
|
-
const read = await ticketChildRead(http, { globalPath: "/V1.0/TicketAttachments/query", resourceKey: "ticket_attachments", filter: ticketFilter, limit: DEFAULT_PART_LIMIT, operation: op }, ticketId);
|
|
8324
|
-
attachmentsStatus = read.status;
|
|
8325
|
-
attachments = {
|
|
8326
|
-
ids: read.items.map((r) => Number(r.id)),
|
|
8327
|
-
names: read.items.map((r) => typeof r.title === "string" ? r.title : null),
|
|
8328
|
-
capped: read.capped
|
|
8329
|
-
};
|
|
8330
|
-
applyRouteStatus(attachments, read);
|
|
8331
|
-
}
|
|
8332
|
-
const result = {
|
|
8333
|
-
ticket: ctx.ticket,
|
|
8334
|
-
company: ctx.company,
|
|
8335
|
-
contact: ctx.contact,
|
|
8336
|
-
assignedResource: ctx.resource,
|
|
8337
|
-
project: ctx.project,
|
|
8338
|
-
notes: notesPart,
|
|
8339
|
-
timeEntries: entriesPart,
|
|
8340
|
-
attachments,
|
|
8341
|
-
truncated: notesPart.capped || entriesPart.capped || attachments.capped || notesTextTruncated,
|
|
8342
|
-
parts: {
|
|
8343
|
-
company: ctx.company !== null ? "ok" : "absent",
|
|
8344
|
-
contact: ctx.contact !== null ? "ok" : "absent",
|
|
8345
|
-
assignedResource: ctx.resource !== null ? "ok" : "absent",
|
|
8346
|
-
project: ctx.project !== null ? "ok" : "absent",
|
|
8347
|
-
notes: notes.status,
|
|
8348
|
-
timeEntries: entries.status,
|
|
8349
|
-
attachments: attachmentsStatus
|
|
8350
|
-
}
|
|
8351
|
-
};
|
|
8352
|
-
if (opts.includeHistories === true) {
|
|
8353
|
-
const historiesRead = await ticketChildRead(http, { globalPath: "/V1.0/TicketHistory/query", resourceKey: "ticket_history", filter: ticketFilter, limit: DEFAULT_PART_LIMIT, operation: op }, ticketId);
|
|
8354
|
-
const historyRows = historiesRead.items.map((h) => servedTextRecord(h, "detail", textLimit));
|
|
8355
|
-
const part = { items: historyRows, capped: historiesRead.capped };
|
|
8356
|
-
applyRouteStatus(part, historiesRead);
|
|
8357
|
-
result.histories = part;
|
|
8358
|
-
result.parts.histories = historiesRead.status;
|
|
8359
|
-
result.truncated = result.truncated || part.capped || historyRows.some((h) => h.detailTruncated === true);
|
|
8360
|
-
}
|
|
8361
|
-
if (opts.names === true) await addCompositeNames(http, ctx, entriesPart, result, opts);
|
|
8362
|
-
return result;
|
|
8363
|
-
}
|
|
8364
|
-
|
|
8365
|
-
// src/resources/special/tickets/forResource.ts
|
|
8366
|
-
var OP2 = "tickets.forResource";
|
|
8367
|
-
var TICKET_CHUNK = 100;
|
|
8368
|
-
var LIMIT_DEFAULT = 100;
|
|
8369
|
-
var LIMIT_MAX = 500;
|
|
8370
|
-
var LINK_READ_CAP = 500;
|
|
8371
|
-
var COUNT_LINK_PAGES = 20;
|
|
8372
|
-
var TICKET_FIELDS = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
8373
|
-
function positiveId(value, name) {
|
|
8374
|
-
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
8375
|
-
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
8376
|
-
throw new AutotaskConfigError(`${OP2}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP2 });
|
|
8377
|
-
}
|
|
8378
|
-
return n;
|
|
8379
|
-
}
|
|
8380
|
-
async function ticketPage(http, filter, maxRecords, fields) {
|
|
8381
|
-
const res = await http.request({
|
|
8382
|
-
method: "POST",
|
|
8383
|
-
idempotent: true,
|
|
8384
|
-
path: "/V1.0/Tickets/query",
|
|
8385
|
-
body: { MaxRecords: maxRecords, filter, IncludeFields: fields },
|
|
8386
|
-
operation: OP2
|
|
8387
|
-
});
|
|
8388
|
-
const page = unwrapItems(res, OP2);
|
|
8389
|
-
return { rows: page.items, more: page.pageDetails.nextPageUrl !== null };
|
|
8390
|
-
}
|
|
8391
|
-
async function readLinkIds(http, resourceID, maxPages = 1) {
|
|
8392
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8393
|
-
let cursor = null;
|
|
8394
|
-
for (let pages = 0; pages < maxPages; pages++) {
|
|
8395
|
-
const res = await http.request({
|
|
8396
|
-
method: "POST",
|
|
8397
|
-
idempotent: true,
|
|
8398
|
-
path: cursor === null ? "/V1.0/TicketSecondaryResources/query" : cursorToPath(http.baseUrl, cursor),
|
|
8399
|
-
body: { MaxRecords: LINK_READ_CAP, filter: [{ field: "resourceID", op: "eq", value: resourceID }], IncludeFields: ["ticketID"] },
|
|
8400
|
-
operation: OP2
|
|
8401
|
-
});
|
|
8402
|
-
const page = unwrapItems(res, OP2);
|
|
8403
|
-
for (const row of page.items) if (typeof row.ticketID === "number") ids.add(row.ticketID);
|
|
8404
|
-
cursor = page.pageDetails.nextPageUrl;
|
|
8405
|
-
if (cursor === null || page.items.length === 0) return { ids: [...ids], capped: false };
|
|
8406
|
-
}
|
|
8407
|
-
return { ids: [...ids], capped: true };
|
|
8408
|
-
}
|
|
8409
|
-
async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
8410
|
-
if (opts === null || typeof opts !== "object") throw new AutotaskConfigError(`${OP2}: opts must be an object`, { operation: OP2 });
|
|
8411
|
-
const resourceID = positiveId(resourceIDIn, "resourceID");
|
|
8412
|
-
const scope = opts.scope ?? "both";
|
|
8413
|
-
if (scope !== "primary" && scope !== "secondary" && scope !== "both") {
|
|
8414
|
-
throw new AutotaskConfigError(`${OP2}: scope must be 'primary', 'secondary' or 'both' (got ${JSON.stringify(opts.scope)})`, { operation: OP2 });
|
|
8078
|
+
return { ids: [...ids], capped: true };
|
|
8079
|
+
}
|
|
8080
|
+
async function forResource(_resource, resourceIDIn, opts = {}) {
|
|
8081
|
+
const http = _resource.http;
|
|
8082
|
+
if (opts === null || typeof opts !== "object") throw new AutotaskConfigError(`${OP4}: opts must be an object`, { operation: OP4 });
|
|
8083
|
+
const resourceID = positiveId(resourceIDIn, "resourceID");
|
|
8084
|
+
const scope = opts.scope ?? "both";
|
|
8085
|
+
if (scope !== "primary" && scope !== "secondary" && scope !== "both") {
|
|
8086
|
+
throw new AutotaskConfigError(`${OP4}: scope must be 'primary', 'secondary' or 'both' (got ${JSON.stringify(opts.scope)})`, { operation: OP4 });
|
|
8415
8087
|
}
|
|
8416
8088
|
if (opts.openOnly !== void 0 && typeof opts.openOnly !== "boolean") {
|
|
8417
|
-
throw new AutotaskConfigError(`${
|
|
8089
|
+
throw new AutotaskConfigError(`${OP4}: openOnly must be a boolean`, { operation: OP4 });
|
|
8418
8090
|
}
|
|
8419
8091
|
const limit = opts.limit === void 0 ? LIMIT_DEFAULT : opts.limit;
|
|
8420
8092
|
if (opts.countOnly !== true && (!Number.isInteger(limit) || limit < 1 || limit > LIMIT_MAX)) {
|
|
8421
|
-
throw new AutotaskConfigError(`${
|
|
8093
|
+
throw new AutotaskConfigError(`${OP4}: limit must be an integer 1-${LIMIT_MAX} (got ${String(opts.limit)})`, { operation: OP4 });
|
|
8422
8094
|
}
|
|
8423
8095
|
const includeFields = opts.includeFields ?? [];
|
|
8424
8096
|
if (!Array.isArray(includeFields) || includeFields.some((f) => typeof f !== "string" || f.length === 0)) {
|
|
8425
|
-
throw new AutotaskConfigError(`${
|
|
8097
|
+
throw new AutotaskConfigError(`${OP4}: includeFields must be a list of field names`, { operation: OP4 });
|
|
8426
8098
|
}
|
|
8427
8099
|
const openOnly = opts.openOnly === true;
|
|
8428
8100
|
const fields = [.../* @__PURE__ */ new Set([...TICKET_FIELDS, ...includeFields])];
|
|
8429
8101
|
if (opts.countOnly !== void 0 && typeof opts.countOnly !== "boolean") {
|
|
8430
|
-
throw new AutotaskConfigError(`${
|
|
8102
|
+
throw new AutotaskConfigError(`${OP4}: countOnly must be a boolean`, { operation: OP4 });
|
|
8431
8103
|
}
|
|
8432
8104
|
const closed = await resolveTicketStatusSemantics(
|
|
8433
8105
|
http,
|
|
8434
8106
|
{ ...opts.excludeStatusIds === void 0 ? {} : { excludeStatusIds: opts.excludeStatusIds }, ...opts.allowAssumed === void 0 ? {} : { allowAssumed: opts.allowAssumed } },
|
|
8435
|
-
|
|
8107
|
+
OP4
|
|
8436
8108
|
);
|
|
8437
8109
|
const closedStatus = { closedIds: closed.closedIds, source: closed.source, note: closed.note };
|
|
8438
8110
|
const openLeaf = openOnly || opts.countOnly === true ? [openStatusFilter(closed.closedIds)] : [];
|
|
@@ -8519,74 +8191,483 @@ async function forResource(_resource, resourceIDIn, http, opts = {}) {
|
|
|
8519
8191
|
closedStatus
|
|
8520
8192
|
};
|
|
8521
8193
|
}
|
|
8522
|
-
|
|
8523
|
-
// src/resources/special/tickets/
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
}
|
|
8530
|
-
|
|
8194
|
+
|
|
8195
|
+
// src/resources/special/tickets/common.ts
|
|
8196
|
+
async function fetchTicket(http, id, operation) {
|
|
8197
|
+
const res = await http.request({
|
|
8198
|
+
method: "GET",
|
|
8199
|
+
path: `/V1.0/Tickets/${id}`,
|
|
8200
|
+
operation
|
|
8201
|
+
});
|
|
8202
|
+
const body = res;
|
|
8203
|
+
if (body === null || !("item" in body)) {
|
|
8204
|
+
throw new NotFoundError(`Ticket ${id} not found (unexpected envelope shape)`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
|
|
8205
|
+
}
|
|
8206
|
+
if (body.item === null || body.item === void 0) throw new NotFoundError(`Ticket ${id} not found`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
|
|
8207
|
+
return body.item;
|
|
8208
|
+
}
|
|
8209
|
+
async function ticketQueryItems(http, path, filter, maxRecords, operation) {
|
|
8210
|
+
const res = await http.request({
|
|
8211
|
+
method: "POST",
|
|
8212
|
+
idempotent: true,
|
|
8213
|
+
path,
|
|
8214
|
+
body: { MaxRecords: maxRecords, filter },
|
|
8215
|
+
operation
|
|
8216
|
+
});
|
|
8217
|
+
return unwrapItems(res).items;
|
|
8218
|
+
}
|
|
8219
|
+
function truncate(text, limit) {
|
|
8220
|
+
const full = text ?? "";
|
|
8221
|
+
if (full.length <= limit) return { value: full, truncated: false, length: full.length };
|
|
8222
|
+
return { value: `${full.slice(0, limit)}\u2026`, truncated: true, length: full.length };
|
|
8223
|
+
}
|
|
8224
|
+
|
|
8225
|
+
// src/resources/special/tickets/childRead.ts
|
|
8226
|
+
function isRouteUnavailable(err) {
|
|
8227
|
+
return isAutotaskError(err) && (err.httpStatus === 404 || err.httpStatus === 500);
|
|
8228
|
+
}
|
|
8229
|
+
function routeFact(route, path, err) {
|
|
8230
|
+
return {
|
|
8231
|
+
route,
|
|
8232
|
+
path,
|
|
8233
|
+
httpStatus: isAutotaskError(err) && typeof err.httpStatus === "number" ? err.httpStatus : null
|
|
8234
|
+
};
|
|
8235
|
+
}
|
|
8236
|
+
function childDeclaresQuery(resourceKey) {
|
|
8237
|
+
return CAPABILITIES.some(
|
|
8238
|
+
(c) => c.id === `${resourceKey}.query` && (c.status === "implemented" || c.status === "tested")
|
|
8239
|
+
);
|
|
8240
|
+
}
|
|
8241
|
+
function planScopedPath(resourceKey, ticketId) {
|
|
8242
|
+
if (!childDeclaresQuery(resourceKey)) return null;
|
|
8243
|
+
const entry = RELATIONSHIP_MAP[resourceKey];
|
|
8244
|
+
const parent = entry === void 0 ? null : entry.parent;
|
|
8245
|
+
if (parent === null || parent.resource !== "tickets" || parent.segment === null || parent.field === null) return null;
|
|
8246
|
+
const parentPath = RELATIONSHIP_MAP[parent.resource]?.path;
|
|
8247
|
+
if (parentPath === null || parentPath === void 0) return null;
|
|
8248
|
+
return `/V1.0/${parentPath}/${ticketId}/${parent.segment}`;
|
|
8249
|
+
}
|
|
8250
|
+
function scopedRowMatches(row, filter) {
|
|
8251
|
+
let conds;
|
|
8252
|
+
if (Array.isArray(filter)) conds = filter;
|
|
8253
|
+
else if (filter !== null && typeof filter === "object" && Array.isArray(filter.filter)) {
|
|
8254
|
+
conds = filter.filter;
|
|
8255
|
+
}
|
|
8256
|
+
if (conds === void 0) return true;
|
|
8257
|
+
for (const raw of conds) {
|
|
8258
|
+
if (raw === null || typeof raw !== "object") return false;
|
|
8259
|
+
const cond = raw;
|
|
8260
|
+
if (typeof cond.field !== "string" || typeof cond.op !== "string") return false;
|
|
8261
|
+
if (cond.field === "ticketID") continue;
|
|
8262
|
+
const value = row[cond.field];
|
|
8263
|
+
if (typeof value !== "string") return false;
|
|
8264
|
+
const want = String(cond.value ?? "");
|
|
8265
|
+
if (cond.op === "eq" && value !== want) return false;
|
|
8266
|
+
if (cond.op === "gt" && !(value > want)) return false;
|
|
8267
|
+
if (cond.op === "lt" && !(value < want)) return false;
|
|
8268
|
+
if (cond.op !== "eq" && cond.op !== "gt" && cond.op !== "lt") return false;
|
|
8269
|
+
}
|
|
8270
|
+
return true;
|
|
8271
|
+
}
|
|
8272
|
+
var SCOPED_FALLBACK_PAGE_CAP = 10;
|
|
8273
|
+
async function scopedCollectionRead(http, startPath, limit, operation, accept) {
|
|
8274
|
+
let target = startPath;
|
|
8275
|
+
const items = [];
|
|
8276
|
+
let guard = false;
|
|
8277
|
+
let pages = 0;
|
|
8278
|
+
let nextCursor = null;
|
|
8279
|
+
for (; ; ) {
|
|
8280
|
+
if (pages >= SCOPED_FALLBACK_PAGE_CAP) {
|
|
8281
|
+
guard = true;
|
|
8282
|
+
break;
|
|
8283
|
+
}
|
|
8284
|
+
const body = await http.request({ method: "GET", path: target, operation });
|
|
8285
|
+
const { items: pageItems, pageDetails } = unwrapItems(body, operation);
|
|
8286
|
+
nextCursor = pageDetails.nextPageUrl;
|
|
8287
|
+
pages += 1;
|
|
8288
|
+
const window = pageItems.slice(0, Math.max(0, limit - items.length));
|
|
8289
|
+
for (const row of window) if (accept(row)) items.push(row);
|
|
8290
|
+
if (items.length >= limit) break;
|
|
8291
|
+
if (nextCursor === null || pageItems.length === 0) break;
|
|
8292
|
+
target = cursorToPath(http.baseUrl, nextCursor);
|
|
8293
|
+
}
|
|
8294
|
+
const capped = items.length >= limit || guard && nextCursor !== null;
|
|
8295
|
+
return { items, capped };
|
|
8296
|
+
}
|
|
8297
|
+
async function ticketChildRead(http, spec, ticketId) {
|
|
8298
|
+
try {
|
|
8299
|
+
const items = await ticketQueryItems(http, spec.globalPath, spec.filter, spec.limit, spec.operation);
|
|
8300
|
+
return { items, capped: items.length >= spec.limit, status: "ok", routeFacts: [] };
|
|
8301
|
+
} catch (err) {
|
|
8302
|
+
if (!isRouteUnavailable(err)) throw err;
|
|
8303
|
+
const facts = [routeFact("global", spec.globalPath, err)];
|
|
8304
|
+
const scopedPath = planScopedPath(spec.resourceKey, ticketId);
|
|
8305
|
+
if (scopedPath === null) return { items: [], capped: false, status: "miss", routeFacts: facts };
|
|
8306
|
+
try {
|
|
8307
|
+
const read = await scopedCollectionRead(http, scopedPath, spec.limit, spec.operation, (row) => scopedRowMatches(row, spec.filter));
|
|
8308
|
+
return { ...read, status: "ok-scoped", routeFacts: [] };
|
|
8309
|
+
} catch (scopedErr) {
|
|
8310
|
+
facts.push(routeFact("scoped", scopedPath, scopedErr));
|
|
8311
|
+
return { items: [], capped: false, status: "miss", routeFacts: facts };
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
function applyRouteStatus(part, read) {
|
|
8316
|
+
if (read.status === "ok-scoped") part.route = "scoped";
|
|
8317
|
+
if (read.status === "miss") part.routeFacts = read.routeFacts;
|
|
8318
|
+
}
|
|
8319
|
+
|
|
8320
|
+
// src/resources/special/tickets/fullContext.ts
|
|
8321
|
+
var DEFAULT_PART_LIMIT = 50;
|
|
8322
|
+
function partLimit(value, name) {
|
|
8323
|
+
const n = value === void 0 ? DEFAULT_PART_LIMIT : Math.trunc(value);
|
|
8324
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
8325
|
+
throw new AutotaskConfigError(`fullContext: ${name} must be an integer >= 1 (got ${String(value)})`);
|
|
8326
|
+
}
|
|
8327
|
+
return n;
|
|
8328
|
+
}
|
|
8329
|
+
function servedTextRecord(row, textField, limit) {
|
|
8330
|
+
const text = row[textField];
|
|
8331
|
+
if (typeof text !== "string" || text.length <= limit) return { ...row };
|
|
8332
|
+
const out = { ...row };
|
|
8333
|
+
out[`${textField}Truncated`] = true;
|
|
8334
|
+
out[textField] = `${text.slice(0, limit)}\u2026`;
|
|
8335
|
+
return out;
|
|
8336
|
+
}
|
|
8337
|
+
async function addCompositeNames(http, ctx, entriesPart, result, opts) {
|
|
8338
|
+
const builder = createNamePlan(TICKET_LABEL_FIELDS);
|
|
8339
|
+
const t = ctx.ticket;
|
|
8340
|
+
builder.addResource(t["creatorResourceID"]);
|
|
8341
|
+
builder.addFk("contract", t["contractID"]);
|
|
8342
|
+
builder.addFk("billingCode", t["billingCodeID"]);
|
|
8343
|
+
builder.addFk("role", t["assignedResourceRoleID"]);
|
|
8344
|
+
const ticketLobPointer = vendorId(t["organizationalLevelAssociationID"]);
|
|
8345
|
+
if (ticketLobPointer !== void 0) {
|
|
8346
|
+
builder.addLineOfBusiness(ticketLobPointer, { source: "ticket" });
|
|
8347
|
+
} else {
|
|
8348
|
+
builder.addLineOfBusinessFromContract(t["contractID"]);
|
|
8349
|
+
}
|
|
8350
|
+
for (const e of entriesPart.items) {
|
|
8351
|
+
builder.addResource(e["resourceID"]);
|
|
8352
|
+
builder.addFk("role", e["roleID"]);
|
|
8353
|
+
builder.addFk("billingCode", e["billingCodeID"]);
|
|
8354
|
+
builder.addFk("contract", e["contractID"]);
|
|
8355
|
+
}
|
|
8356
|
+
const names = await resolveCompositeNames(http, builder.plan(), { maxNameReads: opts.maxNameReads });
|
|
8357
|
+
const ticketRow = { ...ctx.ticket };
|
|
8358
|
+
addNameSibling(ticketRow, "creatorResourceID", (v) => names.resourceName(v));
|
|
8359
|
+
addNameSibling(ticketRow, "contractID", (v) => names.recordName("contract", v));
|
|
8360
|
+
addNameSibling(ticketRow, "billingCodeID", (v) => names.recordName("billingCode", v));
|
|
8361
|
+
addNameSibling(ticketRow, "assignedResourceRoleID", (v) => names.recordName("role", v));
|
|
8362
|
+
for (const f of TICKET_LABEL_FIELDS) addLabelSibling(ticketRow, f, names);
|
|
8363
|
+
result.ticket = ticketRow;
|
|
8364
|
+
result.timeEntries.items = entriesPart.items.map((e) => {
|
|
8365
|
+
const row = { ...e };
|
|
8366
|
+
addNameSibling(row, "resourceID", (v) => names.resourceName(v));
|
|
8367
|
+
addNameSibling(row, "roleID", (v) => names.recordName("role", v));
|
|
8368
|
+
addNameSibling(row, "billingCodeID", (v) => names.recordName("billingCode", v));
|
|
8369
|
+
addNameSibling(row, "contractID", (v) => names.recordName("contract", v));
|
|
8370
|
+
return row;
|
|
8371
|
+
});
|
|
8372
|
+
result.names = names.section;
|
|
8373
|
+
result.lineOfBusiness = names.lineOfBusiness ?? {
|
|
8374
|
+
association: null,
|
|
8375
|
+
organizationalLevel2: null,
|
|
8376
|
+
organizationalLevel1: null,
|
|
8377
|
+
markers: { association: "missing", organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
8378
|
+
};
|
|
8379
|
+
result.truncated = result.truncated || names.section.truncated;
|
|
8380
|
+
}
|
|
8381
|
+
async function fullContext(resource, id, opts = {}) {
|
|
8382
|
+
const http = resource.http;
|
|
8383
|
+
const noteLimit = partLimit(opts.noteLimit, "noteLimit");
|
|
8384
|
+
const timeEntryLimit = partLimit(opts.timeEntryLimit, "timeEntryLimit");
|
|
8385
|
+
const textLimit = partLimit(opts.textLimit, "textLimit");
|
|
8386
|
+
const ticketId = parseIdentifier(id, "id");
|
|
8387
|
+
const ctx = await resource.getContext(ticketId);
|
|
8388
|
+
const op = "tickets.fullContext";
|
|
8389
|
+
const ticketFilter = [{ field: "ticketID", op: "eq", value: ticketId }];
|
|
8390
|
+
const notes = await ticketChildRead(http, { globalPath: "/V1.0/TicketNotes/query", resourceKey: "ticket_notes", filter: ticketFilter, limit: noteLimit, operation: op }, ticketId);
|
|
8391
|
+
const entries = await ticketChildRead(http, { globalPath: "/V1.0/TimeEntries/query", resourceKey: "time_entries", filter: ticketFilter, limit: timeEntryLimit, operation: op }, ticketId);
|
|
8392
|
+
const notesRows = notes.items.map((n) => servedTextRecord(n, "description", textLimit));
|
|
8393
|
+
const notesPart = { items: notesRows, capped: notes.capped };
|
|
8394
|
+
applyRouteStatus(notesPart, notes);
|
|
8395
|
+
const entriesPart = { items: entries.items, capped: entries.capped };
|
|
8396
|
+
applyRouteStatus(entriesPart, entries);
|
|
8397
|
+
const notesTextTruncated = notesRows.some((n) => n.descriptionTruncated === true);
|
|
8398
|
+
let attachments = { ids: [], names: [], capped: false };
|
|
8399
|
+
let attachmentsStatus = "ok";
|
|
8400
|
+
if (opts.includeAttachments !== false) {
|
|
8401
|
+
const read = await ticketChildRead(http, { globalPath: "/V1.0/TicketAttachments/query", resourceKey: "ticket_attachments", filter: ticketFilter, limit: DEFAULT_PART_LIMIT, operation: op }, ticketId);
|
|
8402
|
+
attachmentsStatus = read.status;
|
|
8403
|
+
attachments = {
|
|
8404
|
+
ids: read.items.map((r) => Number(r.id)),
|
|
8405
|
+
names: read.items.map((r) => typeof r.title === "string" ? r.title : null),
|
|
8406
|
+
capped: read.capped
|
|
8407
|
+
};
|
|
8408
|
+
applyRouteStatus(attachments, read);
|
|
8409
|
+
}
|
|
8410
|
+
const result = {
|
|
8411
|
+
ticket: ctx.ticket,
|
|
8412
|
+
company: ctx.company,
|
|
8413
|
+
contact: ctx.contact,
|
|
8414
|
+
assignedResource: ctx.resource,
|
|
8415
|
+
project: ctx.project,
|
|
8416
|
+
notes: notesPart,
|
|
8417
|
+
timeEntries: entriesPart,
|
|
8418
|
+
attachments,
|
|
8419
|
+
truncated: notesPart.capped || entriesPart.capped || attachments.capped || notesTextTruncated,
|
|
8420
|
+
parts: {
|
|
8421
|
+
company: ctx.company !== null ? "ok" : "absent",
|
|
8422
|
+
contact: ctx.contact !== null ? "ok" : "absent",
|
|
8423
|
+
assignedResource: ctx.resource !== null ? "ok" : "absent",
|
|
8424
|
+
project: ctx.project !== null ? "ok" : "absent",
|
|
8425
|
+
notes: notes.status,
|
|
8426
|
+
timeEntries: entries.status,
|
|
8427
|
+
attachments: attachmentsStatus
|
|
8428
|
+
}
|
|
8429
|
+
};
|
|
8430
|
+
if (opts.includeHistories === true) {
|
|
8431
|
+
const historiesRead = await ticketChildRead(http, { globalPath: "/V1.0/TicketHistory/query", resourceKey: "ticket_history", filter: ticketFilter, limit: DEFAULT_PART_LIMIT, operation: op }, ticketId);
|
|
8432
|
+
const historyRows = historiesRead.items.map((h) => servedTextRecord(h, "detail", textLimit));
|
|
8433
|
+
const part = { items: historyRows, capped: historiesRead.capped };
|
|
8434
|
+
applyRouteStatus(part, historiesRead);
|
|
8435
|
+
result.histories = part;
|
|
8436
|
+
result.parts.histories = historiesRead.status;
|
|
8437
|
+
result.truncated = result.truncated || part.capped || historyRows.some((h) => h.detailTruncated === true);
|
|
8438
|
+
}
|
|
8439
|
+
if (opts.names === true) await addCompositeNames(http, ctx, entriesPart, result, opts);
|
|
8440
|
+
return result;
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
// src/resources/special/tickets/lineOfBusiness.ts
|
|
8444
|
+
var OP5 = "tickets.lineOfBusiness";
|
|
8445
|
+
function positiveId2(value, name) {
|
|
8446
|
+
const n = typeof value === "string" && /^[0-9]+$/.test(value.trim()) ? Number(value) : value;
|
|
8447
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
8448
|
+
throw new AutotaskConfigError(OP5 + ": " + name + " must be a positive integer id (got " + String(value) + ")", { operation: OP5 });
|
|
8449
|
+
}
|
|
8450
|
+
return n;
|
|
8451
|
+
}
|
|
8452
|
+
async function lineOfBusiness(resource, ticketIdIn, associationIdIn) {
|
|
8453
|
+
const http = resource.http;
|
|
8454
|
+
const ticketId = positiveId2(ticketIdIn, "ticketId");
|
|
8455
|
+
let associationId;
|
|
8456
|
+
let associationSource = null;
|
|
8457
|
+
let contractId = null;
|
|
8458
|
+
let contractUnread = false;
|
|
8459
|
+
if (associationIdIn !== void 0) {
|
|
8460
|
+
associationId = positiveId2(associationIdIn, "associationId");
|
|
8461
|
+
associationSource = "argument";
|
|
8462
|
+
} else {
|
|
8463
|
+
const ticket = await resource.get(ticketId);
|
|
8464
|
+
associationId = vendorId(ticket.organizationalLevelAssociationID);
|
|
8465
|
+
if (associationId !== void 0) {
|
|
8466
|
+
associationSource = "ticket";
|
|
8467
|
+
} else {
|
|
8468
|
+
const contractRef = vendorId(ticket.contractID);
|
|
8469
|
+
if (contractRef !== void 0) {
|
|
8470
|
+
const client = http.client;
|
|
8471
|
+
if (client === void 0) {
|
|
8472
|
+
throw new AutotaskConfigError(OP5 + ": the transport is not wired to an AutotaskClient (the composite must run through the client)", { operation: OP5 });
|
|
8473
|
+
}
|
|
8474
|
+
try {
|
|
8475
|
+
const contract = await client.contracts.get(contractRef);
|
|
8476
|
+
contractId = contractRef;
|
|
8477
|
+
associationId = vendorId(contract.organizationalLevelAssociationID);
|
|
8478
|
+
if (associationId !== void 0) associationSource = "contract";
|
|
8479
|
+
} catch (e) {
|
|
8480
|
+
if (e instanceof NotFoundError) {
|
|
8481
|
+
contractId = contractRef;
|
|
8482
|
+
} else if (isAutotaskError(e) && e.code !== "ABORTED") {
|
|
8483
|
+
contractId = contractRef;
|
|
8484
|
+
contractUnread = true;
|
|
8485
|
+
} else {
|
|
8486
|
+
throw e;
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
}
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
if (associationId === void 0) {
|
|
8493
|
+
return {
|
|
8494
|
+
ticketId,
|
|
8495
|
+
associationSource,
|
|
8496
|
+
contractId,
|
|
8497
|
+
association: null,
|
|
8498
|
+
organizationalLevel2: null,
|
|
8499
|
+
organizationalLevel1: null,
|
|
8500
|
+
markers: { association: contractUnread ? "unresolved" : "missing", organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
8501
|
+
};
|
|
8502
|
+
}
|
|
8503
|
+
const chain = await resolveLineOfBusinessChain(http, associationId, { budget: 3, operation: OP5, onError: "propagate" });
|
|
8504
|
+
const { association, organizationalLevel2, organizationalLevel1, markers } = chain.lineOfBusiness;
|
|
8505
|
+
return { ticketId, associationSource, contractId, association, organizationalLevel2, organizationalLevel1, markers };
|
|
8506
|
+
}
|
|
8507
|
+
|
|
8508
|
+
// src/resources/special/tickets/sla.ts
|
|
8509
|
+
var MS = 864e5;
|
|
8510
|
+
var SLA_PAGE = 100;
|
|
8511
|
+
async function slaHealthCheck(_resource, id, opts = {}) {
|
|
8512
|
+
const http = _resource.http;
|
|
8513
|
+
void opts;
|
|
8514
|
+
const ticketId = parseIdentifier(id, "id");
|
|
8515
|
+
await fetchTicket(http, ticketId, "tickets.slaHealthCheck");
|
|
8516
|
+
const rows = await ticketQueryItems(
|
|
8517
|
+
http,
|
|
8518
|
+
"/V1.0/ServiceLevelAgreementResults/query",
|
|
8519
|
+
[{ field: "ticketID", op: "eq", value: ticketId }],
|
|
8520
|
+
SLA_PAGE,
|
|
8521
|
+
"tickets.slaHealthCheck"
|
|
8522
|
+
);
|
|
8523
|
+
const slas = rows.map((row) => {
|
|
8524
|
+
const bool = (v) => typeof v === "boolean" ? v : null;
|
|
8525
|
+
const num = (v) => typeof v === "number" ? v : null;
|
|
8526
|
+
const milestones = [
|
|
8527
|
+
{ name: "firstResponse", met: bool(row.isFirstResponseMet), elapsedHours: num(row.firstResponseElapsedHours) },
|
|
8528
|
+
{ name: "resolution", met: bool(row.isResolutionMet), elapsedHours: num(row.resolutionElapsedHours) },
|
|
8529
|
+
{ name: "resolutionPlan", met: bool(row.isResolutionPlanMet), elapsedHours: num(row.resolutionPlanElapsedHours) }
|
|
8530
|
+
];
|
|
8531
|
+
return { id: Number(row.id), name: row.serviceLevelAgreementName ?? null, milestones };
|
|
8532
|
+
});
|
|
8533
|
+
const healthy = slas.every((sla) => sla.milestones.every((m) => m.met === true));
|
|
8534
|
+
return { ticketId, slaCount: slas.length, healthy, slas, truncated: rows.length >= SLA_PAGE };
|
|
8535
|
+
}
|
|
8536
|
+
async function summary(_resource, id, opts = {}) {
|
|
8537
|
+
const http = _resource.http;
|
|
8538
|
+
const limit = opts.summaryTextLimit === void 0 ? 500 : Math.trunc(opts.summaryTextLimit);
|
|
8539
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
8540
|
+
throw new AutotaskConfigError(`summaryTextLimit must be an integer >= 1 (got ${String(opts.summaryTextLimit)})`);
|
|
8541
|
+
}
|
|
8542
|
+
const ticketId = parseIdentifier(id, "id");
|
|
8543
|
+
const ticket = await fetchTicket(http, ticketId, "tickets.summary");
|
|
8544
|
+
const desc = truncate(ticket.description, limit);
|
|
8545
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
8546
|
+
const ageDays = typeof ticket.createDate === "string" && ticket.createDate !== "" ? Math.max(0, Math.floor((now.getTime() - Date.parse(ticket.createDate)) / MS)) : null;
|
|
8547
|
+
const pick = (v) => typeof v === "number" ? v : null;
|
|
8548
|
+
const str = (v) => typeof v === "string" && v !== "" ? v : null;
|
|
8549
|
+
return {
|
|
8550
|
+
ticketId,
|
|
8551
|
+
ticketNumber: str(ticket.ticketNumber),
|
|
8552
|
+
title: str(ticket.title),
|
|
8553
|
+
description: desc.value,
|
|
8554
|
+
descriptionLength: desc.length,
|
|
8555
|
+
descriptionTruncated: desc.truncated,
|
|
8556
|
+
status: pick(ticket.status),
|
|
8557
|
+
priority: pick(ticket.priority),
|
|
8558
|
+
companyID: pick(ticket.companyID),
|
|
8559
|
+
contactID: pick(ticket.contactID),
|
|
8560
|
+
assignedResourceID: pick(ticket.assignedResourceID),
|
|
8561
|
+
projectID: pick(ticket.projectID),
|
|
8562
|
+
createDate: str(ticket.createDate),
|
|
8563
|
+
dueDateTime: str(ticket.dueDateTime),
|
|
8564
|
+
ageDays
|
|
8565
|
+
};
|
|
8566
|
+
}
|
|
8567
|
+
|
|
8568
|
+
// src/resources/special/tickets/statusSemantics.ts
|
|
8569
|
+
async function statusSemantics(_resource, opts = {}) {
|
|
8570
|
+
const http = _resource.http;
|
|
8571
|
+
return resolveTicketStatusSemantics(http, opts);
|
|
8572
|
+
}
|
|
8573
|
+
|
|
8574
|
+
// src/resources/special/tickets/timeline.ts
|
|
8575
|
+
function validateTimelineLimits(opts) {
|
|
8576
|
+
const limit = opts.limit === void 0 ? 100 : Math.trunc(opts.limit);
|
|
8577
|
+
if (!Number.isInteger(limit) || limit < 1) throw new AutotaskConfigError(`limit must be an integer >= 1 (got ${String(opts.limit)})`);
|
|
8578
|
+
const bounded = Math.min(limit, 500);
|
|
8579
|
+
const textLimit = opts.textLimit === void 0 ? 500 : Math.trunc(opts.textLimit);
|
|
8580
|
+
if (!Number.isInteger(textLimit) || textLimit < 1) {
|
|
8581
|
+
throw new AutotaskConfigError(`textLimit must be an integer >= 1 (got ${String(opts.textLimit)})`);
|
|
8582
|
+
}
|
|
8583
|
+
return { bounded, textLimit };
|
|
8584
|
+
}
|
|
8585
|
+
function noteEvent(n, textLimit) {
|
|
8586
|
+
const text = truncate(typeof n.description === "string" ? n.description : null, textLimit);
|
|
8587
|
+
return {
|
|
8588
|
+
kind: "note",
|
|
8589
|
+
id: Number(n.id),
|
|
8590
|
+
date: typeof n.createDateTime === "string" ? n.createDateTime : null,
|
|
8591
|
+
text: text.value || null,
|
|
8592
|
+
title: typeof n.title === "string" ? n.title : null,
|
|
8593
|
+
hours: null,
|
|
8594
|
+
action: null,
|
|
8595
|
+
textTruncated: text.truncated
|
|
8596
|
+
};
|
|
8597
|
+
}
|
|
8598
|
+
function timeEntryEvent(t) {
|
|
8599
|
+
return {
|
|
8600
|
+
kind: "timeEntry",
|
|
8601
|
+
id: Number(t.id),
|
|
8602
|
+
date: typeof t.dateWorked === "string" ? t.dateWorked : null,
|
|
8603
|
+
text: null,
|
|
8604
|
+
title: null,
|
|
8605
|
+
hours: typeof t.hoursWorked === "number" ? t.hoursWorked : null,
|
|
8606
|
+
action: null,
|
|
8607
|
+
textTruncated: false
|
|
8608
|
+
};
|
|
8609
|
+
}
|
|
8610
|
+
function historyEvent(h, textLimit) {
|
|
8611
|
+
const text = truncate(typeof h.detail === "string" ? h.detail : null, textLimit);
|
|
8612
|
+
return {
|
|
8613
|
+
kind: "history",
|
|
8614
|
+
id: Number(h.id),
|
|
8615
|
+
date: typeof h.date === "string" ? h.date : null,
|
|
8616
|
+
text: text.value || null,
|
|
8617
|
+
title: null,
|
|
8618
|
+
hours: null,
|
|
8619
|
+
action: typeof h.action === "string" ? h.action : null,
|
|
8620
|
+
textTruncated: text.truncated
|
|
8621
|
+
};
|
|
8531
8622
|
}
|
|
8532
|
-
|
|
8533
|
-
const
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
} else {
|
|
8565
|
-
throw e;
|
|
8566
|
-
}
|
|
8567
|
-
}
|
|
8568
|
-
}
|
|
8569
|
-
}
|
|
8623
|
+
function compareEventDates(a, b) {
|
|
8624
|
+
const da = a.date ?? "";
|
|
8625
|
+
const db = b.date ?? "";
|
|
8626
|
+
if (da === "" && db === "") return 0;
|
|
8627
|
+
if (da === "") return 1;
|
|
8628
|
+
if (db === "") return -1;
|
|
8629
|
+
return da < db ? -1 : da > db ? 1 : 0;
|
|
8630
|
+
}
|
|
8631
|
+
async function timeline(_resource, id, opts = {}) {
|
|
8632
|
+
const http = _resource.http;
|
|
8633
|
+
const { bounded, textLimit } = validateTimelineLimits(opts);
|
|
8634
|
+
const ticketId = parseIdentifier(id, "id");
|
|
8635
|
+
await fetchTicket(http, ticketId, "tickets.timeline");
|
|
8636
|
+
const dateFilters = (field) => {
|
|
8637
|
+
const f = [{ field: "ticketID", op: "eq", value: ticketId }];
|
|
8638
|
+
if (opts.since !== void 0) f.push({ field, op: "gt", value: opts.since });
|
|
8639
|
+
if (opts.until !== void 0) f.push({ field, op: "lt", value: opts.until });
|
|
8640
|
+
return f;
|
|
8641
|
+
};
|
|
8642
|
+
const events = [];
|
|
8643
|
+
const cappedSources = [];
|
|
8644
|
+
const notes = await ticketChildRead(http, { globalPath: "/V1.0/TicketNotes/query", resourceKey: "ticket_notes", filter: dateFilters("createDateTime"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
8645
|
+
if (notes.capped) cappedSources.push("notes");
|
|
8646
|
+
for (const n of notes.items) events.push(noteEvent(n, textLimit));
|
|
8647
|
+
const entries = await ticketChildRead(http, { globalPath: "/V1.0/TimeEntries/query", resourceKey: "time_entries", filter: dateFilters("dateWorked"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
8648
|
+
if (entries.capped) cappedSources.push("timeEntries");
|
|
8649
|
+
for (const t of entries.items) events.push(timeEntryEvent(t));
|
|
8650
|
+
let historiesRead;
|
|
8651
|
+
if (opts.includeHistories) {
|
|
8652
|
+
historiesRead = await ticketChildRead(http, { globalPath: "/V1.0/TicketHistory/query", resourceKey: "ticket_history", filter: dateFilters("date"), limit: bounded, operation: "tickets.timeline" }, ticketId);
|
|
8653
|
+
if (historiesRead.capped) cappedSources.push("histories");
|
|
8654
|
+
for (const h of historiesRead.items) events.push(historyEvent(h, textLimit));
|
|
8570
8655
|
}
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8656
|
+
events.sort(compareEventDates);
|
|
8657
|
+
const capped = events.slice(0, bounded);
|
|
8658
|
+
const result = {
|
|
8659
|
+
ticketId,
|
|
8660
|
+
count: capped.length,
|
|
8661
|
+
events: capped,
|
|
8662
|
+
truncated: cappedSources.length > 0 || events.length > bounded,
|
|
8663
|
+
cappedSources
|
|
8664
|
+
};
|
|
8665
|
+
if (notes.status !== "ok" || entries.status !== "ok" || historiesRead !== void 0 && historiesRead.status !== "ok") {
|
|
8666
|
+
const routes = { notes: notes.status, timeEntries: entries.status };
|
|
8667
|
+
if (historiesRead !== void 0) routes.histories = historiesRead.status;
|
|
8668
|
+
result.sourceRoutes = routes;
|
|
8581
8669
|
}
|
|
8582
|
-
|
|
8583
|
-
const { association, organizationalLevel2, organizationalLevel1, markers } = chain.lineOfBusiness;
|
|
8584
|
-
return { ticketId, associationSource, contractId, association, organizationalLevel2, organizationalLevel1, markers };
|
|
8585
|
-
}
|
|
8586
|
-
|
|
8587
|
-
// src/resources/special/tickets/statusSemantics.ts
|
|
8588
|
-
async function statusSemantics(_resource, opts = {}, http) {
|
|
8589
|
-
return resolveTicketStatusSemantics(http, opts);
|
|
8670
|
+
return result;
|
|
8590
8671
|
}
|
|
8591
8672
|
|
|
8592
8673
|
// src/resources/tickets.ts
|
|
@@ -8670,40 +8751,36 @@ var TicketsResource = class extends BaseResource {
|
|
|
8670
8751
|
const project = await this.contextFetch("/V1.0/Projects", root["projectID"]);
|
|
8671
8752
|
return { ticket, company, contact, resource, project };
|
|
8672
8753
|
}
|
|
8673
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8754
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/changeStatus.ts). */
|
|
8674
8755
|
changeStatus(id, targetStatus, fields, opts = {}) {
|
|
8675
|
-
return changeStatus(this, id, targetStatus, fields, opts
|
|
8676
|
-
}
|
|
8677
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8678
|
-
openWithNoteAndAssign(params, note, assign, opts = {}) {
|
|
8679
|
-
return openWithNoteAndAssign(this, params, note, assign, opts, this.http);
|
|
8756
|
+
return changeStatus(this, id, targetStatus, fields, opts);
|
|
8680
8757
|
}
|
|
8681
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8758
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/sla.ts). */
|
|
8682
8759
|
slaHealthCheck(id, opts) {
|
|
8683
|
-
return slaHealthCheck(this, id, opts
|
|
8760
|
+
return slaHealthCheck(this, id, opts);
|
|
8684
8761
|
}
|
|
8685
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8762
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/sla.ts). */
|
|
8686
8763
|
summary(id, opts) {
|
|
8687
|
-
return summary(this, id, opts
|
|
8764
|
+
return summary(this, id, opts);
|
|
8688
8765
|
}
|
|
8689
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8766
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/timeline.ts). */
|
|
8690
8767
|
timeline(id, opts) {
|
|
8691
|
-
return timeline(this, id, opts
|
|
8768
|
+
return timeline(this, id, opts);
|
|
8692
8769
|
}
|
|
8693
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8770
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/fullContext.ts). */
|
|
8694
8771
|
fullContext(id, opts) {
|
|
8695
|
-
return fullContext(this, id, opts
|
|
8772
|
+
return fullContext(this, id, opts);
|
|
8696
8773
|
}
|
|
8697
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8774
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/statusSemantics.ts). */
|
|
8698
8775
|
statusSemantics(opts) {
|
|
8699
|
-
return statusSemantics(this, opts
|
|
8776
|
+
return statusSemantics(this, opts);
|
|
8700
8777
|
}
|
|
8701
8778
|
forResource(resourceID, opts) {
|
|
8702
|
-
return forResource(this, resourceID,
|
|
8779
|
+
return forResource(this, resourceID, opts);
|
|
8703
8780
|
}
|
|
8704
|
-
/** Node-parity special op (hand-written in src/resources/special/tickets.ts). */
|
|
8781
|
+
/** Node-parity special op (hand-written in src/resources/special/tickets/lineOfBusiness.ts). */
|
|
8705
8782
|
lineOfBusiness(ticketId, associationId) {
|
|
8706
|
-
return lineOfBusiness(this, ticketId,
|
|
8783
|
+
return lineOfBusiness(this, ticketId, associationId);
|
|
8707
8784
|
}
|
|
8708
8785
|
};
|
|
8709
8786
|
|
|
@@ -8783,6 +8860,356 @@ var TicketWebhooksResource = class extends BaseResource {
|
|
|
8783
8860
|
}
|
|
8784
8861
|
};
|
|
8785
8862
|
|
|
8863
|
+
// src/resources/special/time_entries/move.ts
|
|
8864
|
+
var OP6 = "time_entries.moveToTicket";
|
|
8865
|
+
var COPY2 = ["resourceID", "roleID", "billingCodeID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "offsetHours", "summaryNotes", "internalNotes", "isNonBillable", "showOnInvoice"];
|
|
8866
|
+
var CONTRACT_SCOPED2 = ["contractID", "contractServiceID", "contractServiceBundleID"];
|
|
8867
|
+
async function readTimeEntry(http, id, retries) {
|
|
8868
|
+
try {
|
|
8869
|
+
const res = await http.request({ method: "GET", path: `/V1.0/TimeEntries/${id}`, operation: OP6, maxRetries: retries });
|
|
8870
|
+
return res?.item ?? null;
|
|
8871
|
+
} catch (e) {
|
|
8872
|
+
if (isAutotaskError(e) && (e.httpStatus === 404 || e.code === "NOT_FOUND")) return null;
|
|
8873
|
+
throw e;
|
|
8874
|
+
}
|
|
8875
|
+
}
|
|
8876
|
+
async function checkSource2({ http, source, sourceId, toTicketId, retries }) {
|
|
8877
|
+
const ids = { sourceId, toTicketId };
|
|
8878
|
+
if (source.taskID !== null && source.taskID !== void 0) {
|
|
8879
|
+
throw refuse("MOVE_SOURCE_UNSUPPORTED", `${OP6}: time entry ${sourceId} is on a task \u2014 only ticket time entries are moved`, ids, OP6);
|
|
8880
|
+
}
|
|
8881
|
+
if (source.billingApprovalDateTime || source.billingApprovalResourceID || source.billingApprovalLevelMostRecent) {
|
|
8882
|
+
throw refuse("MOVE_BLOCKED_APPROVED", `${OP6}: time entry ${sourceId} has billing approval set \u2014 move it in the Autotask UI`, ids, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "approved entries are not moved by the API" } });
|
|
8883
|
+
}
|
|
8884
|
+
let billing;
|
|
8885
|
+
try {
|
|
8886
|
+
billing = await queryRows(http, "BillingItems", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP6, retries);
|
|
8887
|
+
} catch {
|
|
8888
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP6}: the billing items of time entry ${sourceId} could not be read \u2014 refused (fails closed)`, ids, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billing state unreadable" } });
|
|
8889
|
+
}
|
|
8890
|
+
if (billing.some((b) => b.invoiceID || b.postedDate)) {
|
|
8891
|
+
throw refuse("MOVE_BLOCKED_BILLED", `${OP6}: time entry ${sourceId} is billed or posted \u2014 move it in the Autotask UI`, ids, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "billed entries are not moved by the API" } });
|
|
8892
|
+
}
|
|
8893
|
+
const attachments = await queryRows(http, "TimeEntryAttachments", [{ field: "timeEntryID", op: "eq", value: sourceId }], OP6, retries);
|
|
8894
|
+
if (attachments.length > 0) {
|
|
8895
|
+
throw refuse("MOVE_BLOCKED_ATTACHMENTS", `${OP6}: time entry ${sourceId} has attachments, which a delete would destroy \u2014 move it in the Autotask UI`, ids, OP6, { manualAction: { action: "delete_source", uiLink: uiLinkFor(http, "time_entries", sourceId), reason: "entries with attachments are not moved by the API" } });
|
|
8896
|
+
}
|
|
8897
|
+
}
|
|
8898
|
+
var SPEC2 = {
|
|
8899
|
+
operation: OP6,
|
|
8900
|
+
entity: "time_entry",
|
|
8901
|
+
readSource: readTimeEntry,
|
|
8902
|
+
sourceTicketId: (s) => typeof s.ticketID === "number" ? s.ticketID : null,
|
|
8903
|
+
dateField: "dateWorked",
|
|
8904
|
+
checkSource: checkSource2,
|
|
8905
|
+
buildPayload(source, toTicketId, { sameScope }) {
|
|
8906
|
+
const body = { ticketID: toTicketId };
|
|
8907
|
+
for (const k of COPY2) if (source[k] !== void 0 && source[k] !== null) body[k] = source[k];
|
|
8908
|
+
if (sameScope) {
|
|
8909
|
+
for (const k of CONTRACT_SCOPED2) if (source[k] !== void 0 && source[k] !== null) body[k] = source[k];
|
|
8910
|
+
}
|
|
8911
|
+
return body;
|
|
8912
|
+
},
|
|
8913
|
+
createPath: () => "/V1.0/TimeEntries",
|
|
8914
|
+
deletePath: (id) => `/V1.0/TimeEntries/${id}`,
|
|
8915
|
+
readById: (http, id) => readTimeEntry(http, id),
|
|
8916
|
+
verifyFields: ["resourceID", "roleID", "billingCodeID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "summaryNotes", "internalNotes", "isNonBillable"],
|
|
8917
|
+
guardFields: ["ticketID", "resourceID", "roleID", "dateWorked", "startDateTime", "endDateTime", "hoursWorked", "summaryNotes", "billingApprovalDateTime", "billingApprovalResourceID", "lastModifiedDateTime"],
|
|
8918
|
+
twinQuery: (p) => ({
|
|
8919
|
+
resource: "TimeEntries",
|
|
8920
|
+
filter: [
|
|
8921
|
+
{ field: "ticketID", op: "eq", value: p.ticketID },
|
|
8922
|
+
{ field: "resourceID", op: "eq", value: p.resourceID },
|
|
8923
|
+
{ field: "dateWorked", op: "eq", value: p.dateWorked },
|
|
8924
|
+
{ field: "hoursWorked", op: "eq", value: p.hoursWorked }
|
|
8925
|
+
]
|
|
8926
|
+
}),
|
|
8927
|
+
manualLink: (http, source) => uiLinkFor(http, "time_entries", Number(source.id))
|
|
8928
|
+
};
|
|
8929
|
+
function moveToTicket2(resource, sourceId, toTicketId, opts) {
|
|
8930
|
+
return runMove(resource.http, SPEC2, sourceId, toTicketId, opts);
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8933
|
+
// src/resources/special/time_entries/posted.ts
|
|
8934
|
+
async function postedStateOf(http, entries, op, beforeRequest) {
|
|
8935
|
+
const states = /* @__PURE__ */ new Map();
|
|
8936
|
+
const ids = [...new Set(entries.map((e) => e.id))];
|
|
8937
|
+
let incomplete = false;
|
|
8938
|
+
for (let c = 0; c < ids.length; c += FETCH_BY_IDS_CHUNK_SIZE) {
|
|
8939
|
+
const chunk = ids.slice(c, c + FETCH_BY_IDS_CHUNK_SIZE);
|
|
8940
|
+
beforeRequest?.();
|
|
8941
|
+
const res = await http.request({
|
|
8942
|
+
method: "POST",
|
|
8943
|
+
idempotent: true,
|
|
8944
|
+
path: "/V1.0/BillingItems/query",
|
|
8945
|
+
body: { MaxRecords: 500, IncludeFields: ["id", "timeEntryID"], filter: [{ field: "timeEntryID", op: "in", value: chunk }] },
|
|
8946
|
+
operation: op
|
|
8947
|
+
});
|
|
8948
|
+
const { items, pageDetails } = unwrapItems(res);
|
|
8949
|
+
const billed = new Set(items.map((i) => i.timeEntryID));
|
|
8950
|
+
const more = pageDetails.nextPageUrl != null;
|
|
8951
|
+
if (more) incomplete = true;
|
|
8952
|
+
for (const id of chunk) {
|
|
8953
|
+
if (billed.has(id)) states.set(id, "posted");
|
|
8954
|
+
else if (!more) states.set(id, "unposted");
|
|
8955
|
+
}
|
|
8956
|
+
}
|
|
8957
|
+
return { states, incomplete };
|
|
8958
|
+
}
|
|
8959
|
+
|
|
8960
|
+
// src/resources/special/time_entries/windows.ts
|
|
8961
|
+
var DAY_MS = 864e5;
|
|
8962
|
+
var PAGE = 500;
|
|
8963
|
+
var COUNT_BUDGET_DEFAULT = 30;
|
|
8964
|
+
var COUNT_BUDGET_MAX = 200;
|
|
8965
|
+
var CURSOR_MAX = 2048;
|
|
8966
|
+
var YMD_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
8967
|
+
function ymdDayMs(value, label, OP7) {
|
|
8968
|
+
const m = YMD_RE.exec(value.trim());
|
|
8969
|
+
if (!m) {
|
|
8970
|
+
throw new AutotaskConfigError(`${OP7}: ${label} must be a UTC 'YYYY-MM-DD' date (got ${JSON.stringify(value)})`, { operation: OP7 });
|
|
8971
|
+
}
|
|
8972
|
+
const y = Number(m[1]);
|
|
8973
|
+
const mo = Number(m[2]);
|
|
8974
|
+
const d = Number(m[3]);
|
|
8975
|
+
const probe = new Date(Date.UTC(y, mo - 1, d));
|
|
8976
|
+
if (probe.getUTCFullYear() !== y || probe.getUTCMonth() !== mo - 1 || probe.getUTCDate() !== d) {
|
|
8977
|
+
throw new AutotaskConfigError(`${OP7}: ${label} is not a valid UTC date (${JSON.stringify(value)})`, { operation: OP7 });
|
|
8978
|
+
}
|
|
8979
|
+
return Date.UTC(y, mo - 1, d);
|
|
8980
|
+
}
|
|
8981
|
+
function windowBounds(from, to, OP7) {
|
|
8982
|
+
const fromMs = ymdDayMs(from, "from", OP7);
|
|
8983
|
+
const toMs = ymdDayMs(to, "to", OP7);
|
|
8984
|
+
if (fromMs > toMs) {
|
|
8985
|
+
throw new AutotaskConfigError(`${OP7}: from (${from}) must not be after to (${to})`, { operation: OP7 });
|
|
8986
|
+
}
|
|
8987
|
+
return { fromMs, toMs };
|
|
8988
|
+
}
|
|
8989
|
+
var iso = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
|
|
8990
|
+
var ENTRY_CURSOR_PATH = "/V1.0/TimeEntries/query/next";
|
|
8991
|
+
function entryCursorPath(baseUrl, cursor) {
|
|
8992
|
+
const path = cursorToPath(baseUrl, cursor);
|
|
8993
|
+
if (path.split("?")[0] !== ENTRY_CURSOR_PATH) {
|
|
8994
|
+
throw new ResponseRejectedError(`Refused the pagination cursor: not a TimeEntries continuation (${ENTRY_CURSOR_PATH})`);
|
|
8995
|
+
}
|
|
8996
|
+
return path;
|
|
8997
|
+
}
|
|
8998
|
+
async function queryTimeEntries(_resource, filter, limit, operation, http, cursor) {
|
|
8999
|
+
const res = await http.request({
|
|
9000
|
+
method: "POST",
|
|
9001
|
+
idempotent: true,
|
|
9002
|
+
path: cursor === void 0 ? "/V1.0/TimeEntries/query" : entryCursorPath(http.baseUrl, cursor),
|
|
9003
|
+
body: { MaxRecords: limit, filter },
|
|
9004
|
+
operation
|
|
9005
|
+
});
|
|
9006
|
+
const { items, pageDetails } = unwrapItems(res);
|
|
9007
|
+
return { items, hasMore: pageDetails.nextPageUrl != null, nextCursor: pageDetails.nextPageUrl ?? null };
|
|
9008
|
+
}
|
|
9009
|
+
function pagingOpts(opts, OP7) {
|
|
9010
|
+
if (opts?.countOnly !== void 0 && typeof opts.countOnly !== "boolean") {
|
|
9011
|
+
throw new AutotaskConfigError(`${OP7}: countOnly must be a boolean`, { operation: OP7 });
|
|
9012
|
+
}
|
|
9013
|
+
const countOnly = opts?.countOnly === true;
|
|
9014
|
+
const cursor = opts?.cursor;
|
|
9015
|
+
if (cursor !== void 0 && (typeof cursor !== "string" || cursor.trim() === "")) {
|
|
9016
|
+
throw new AutotaskConfigError(`${OP7}: cursor must be the non-empty nextCursor of a previous call`, { operation: OP7 });
|
|
9017
|
+
}
|
|
9018
|
+
if (cursor !== void 0 && cursor.length > CURSOR_MAX) {
|
|
9019
|
+
throw new ResponseRejectedError(`${OP7}: refused the pagination cursor: ${cursor.length} chars is over the ${CURSOR_MAX}-char cap (a vendor nextPageUrl is far shorter)`);
|
|
9020
|
+
}
|
|
9021
|
+
if (cursor !== void 0 && countOnly) {
|
|
9022
|
+
throw new AutotaskConfigError(`${OP7}: cursor does not apply with countOnly (countOnly reads every page itself)`, { operation: OP7 });
|
|
9023
|
+
}
|
|
9024
|
+
const budget = opts?.maxVendorCalls ?? COUNT_BUDGET_DEFAULT;
|
|
9025
|
+
if (!Number.isInteger(budget) || budget < 1 || budget > COUNT_BUDGET_MAX) {
|
|
9026
|
+
throw new AutotaskConfigError(`${OP7}: maxVendorCalls must be an integer 1-${COUNT_BUDGET_MAX} (got ${String(opts?.maxVendorCalls)})`, { operation: OP7 });
|
|
9027
|
+
}
|
|
9028
|
+
return { ...cursor === void 0 ? {} : { cursor }, countOnly, budget };
|
|
9029
|
+
}
|
|
9030
|
+
function budgetExceeded(OP7, details, midWalk) {
|
|
9031
|
+
const { estimate, budget, total } = details;
|
|
9032
|
+
const raise = midWalk ? budget < COUNT_BUDGET_MAX ? `, or pass a maxVendorCalls above ${budget} for headroom past the estimate` : "" : estimate <= COUNT_BUDGET_MAX ? ` or pass maxVendorCalls >= ${estimate}` : "";
|
|
9033
|
+
return budgetExceededError(
|
|
9034
|
+
OP7,
|
|
9035
|
+
midWalk ? `${OP7}: countOnly stopped at the ${budget}-call budget mid-walk: the walk needed more calls than the ${estimate} estimated from the ${total}-entry count (more entries than counted, or an extra page such as an empty continuation page), so the next call, which may be the closing re-count, did not fit; no partial total is returned` : `${OP7}: countOnly needs an estimated ${estimate} vendor calls for ${total} entries (1 count + 1 per ${PAGE} entries + 1 join per 250 + 1 re-count), over the ${budget}-call budget; nothing was read past the count`,
|
|
9036
|
+
details,
|
|
9037
|
+
`Narrow the dateWorked window (from/to)${raise}.`
|
|
9038
|
+
);
|
|
9039
|
+
}
|
|
9040
|
+
async function countPostedState(http, filter, OP7, budget, window) {
|
|
9041
|
+
let estimate = 0;
|
|
9042
|
+
let counted = 0;
|
|
9043
|
+
const callBudget = makeCallBudget(budget, (spent) => budgetExceeded(OP7, { estimate, budget, total: counted, issued: spent }, true));
|
|
9044
|
+
const spend = callBudget.spend;
|
|
9045
|
+
spend();
|
|
9046
|
+
counted = unwrapQueryCount(
|
|
9047
|
+
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation: OP7 }),
|
|
9048
|
+
OP7
|
|
9049
|
+
);
|
|
9050
|
+
estimate = 1 + Math.max(1, Math.ceil(counted / PAGE)) + Math.ceil(counted / 250) + 1;
|
|
9051
|
+
if (estimate > budget) throw budgetExceeded(OP7, { estimate, budget, total: counted, issued: callBudget.spent() }, false);
|
|
9052
|
+
const states = /* @__PURE__ */ new Map();
|
|
9053
|
+
let cursor = null;
|
|
9054
|
+
do {
|
|
9055
|
+
spend();
|
|
9056
|
+
const res = await http.request({
|
|
9057
|
+
method: "POST",
|
|
9058
|
+
idempotent: true,
|
|
9059
|
+
path: cursor === null ? "/V1.0/TimeEntries/query" : entryCursorPath(http.baseUrl, cursor),
|
|
9060
|
+
body: { MaxRecords: PAGE, IncludeFields: ["id"], filter },
|
|
9061
|
+
operation: OP7
|
|
9062
|
+
});
|
|
9063
|
+
const page = unwrapItems(res);
|
|
9064
|
+
const joined = await postedStateOf(http, page.items, OP7, spend);
|
|
9065
|
+
if (joined.incomplete) {
|
|
9066
|
+
throw new AutotaskError(`${OP7}: countOnly refused: a billing-item join page was truncated (more than 500 billing items for 250 entries), so the posted split cannot be confirmed`, {
|
|
9067
|
+
code: "RESOLUTION_TRUNCATED",
|
|
9068
|
+
category: "resolution",
|
|
9069
|
+
operation: OP7,
|
|
9070
|
+
details: { issued: callBudget.spent(), budget },
|
|
9071
|
+
suggestedAction: "Narrow the dateWorked window, or page it with cursor (rows mode reports unconfirmed entries via truncated)."
|
|
9072
|
+
});
|
|
9073
|
+
}
|
|
9074
|
+
for (const [id, state] of joined.states) states.set(id, state);
|
|
9075
|
+
cursor = page.items.length === 0 ? null : page.pageDetails.nextPageUrl ?? null;
|
|
9076
|
+
} while (cursor !== null);
|
|
9077
|
+
spend();
|
|
9078
|
+
const recounted = unwrapQueryCount(
|
|
9079
|
+
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation: OP7 }),
|
|
9080
|
+
OP7
|
|
9081
|
+
);
|
|
9082
|
+
if (states.size !== counted && states.size !== recounted) {
|
|
9083
|
+
throw new AutotaskError(`${OP7}: countOnly read ${states.size} distinct entries but the window counted ${counted} before the walk and ${recounted} after it (a page repeated or dropped entries, or the window changed mid-walk); no total is returned. Retry the call`, {
|
|
9084
|
+
code: "INVALID_RESPONSE",
|
|
9085
|
+
category: "server",
|
|
9086
|
+
operation: OP7,
|
|
9087
|
+
details: { reason: "count_mismatch", streamed: states.size, counted, recounted, issued: callBudget.spent(), budget },
|
|
9088
|
+
suggestedAction: "Retry the call; a window that changes during the walk can fail or pass depending on timing."
|
|
9089
|
+
});
|
|
9090
|
+
}
|
|
9091
|
+
let postedCount = 0;
|
|
9092
|
+
for (const state of states.values()) if (state === "posted") postedCount += 1;
|
|
9093
|
+
return { window, total: states.size, postedCount, unpostedCount: states.size - postedCount, vendorCalls: callBudget.spent(), estimate, budget };
|
|
9094
|
+
}
|
|
9095
|
+
async function dateWorkedWindow(resource, from, to, opts) {
|
|
9096
|
+
const http = resource.http;
|
|
9097
|
+
const OP7 = "time_entries.dateWorkedWindow";
|
|
9098
|
+
const paging = pagingOpts(opts, OP7);
|
|
9099
|
+
const { fromMs, toMs } = windowBounds(from, to, OP7);
|
|
9100
|
+
const windowToMs = toMs + DAY_MS - 1;
|
|
9101
|
+
const filter = [
|
|
9102
|
+
{ field: "dateWorked", op: "gte", value: iso(fromMs) },
|
|
9103
|
+
{ field: "dateWorked", op: "lte", value: iso(windowToMs) }
|
|
9104
|
+
];
|
|
9105
|
+
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
9106
|
+
if (opts?.resourceID !== void 0) filter.push({ field: "resourceID", op: "eq", value: opts.resourceID });
|
|
9107
|
+
const window = { from: iso(fromMs), to: iso(windowToMs) };
|
|
9108
|
+
if (paging.countOnly) return countPostedState(http, filter, OP7, paging.budget, window);
|
|
9109
|
+
const { items, hasMore, nextCursor } = await queryTimeEntries(resource, filter, clampWindowLimit(opts?.limit, OP7), OP7, http, paging.cursor);
|
|
9110
|
+
const { states, incomplete } = await postedStateOf(http, items, OP7);
|
|
9111
|
+
let postedCount = 0;
|
|
9112
|
+
let unpostedCount = 0;
|
|
9113
|
+
for (const state of states.values()) {
|
|
9114
|
+
if (state === "posted") postedCount += 1;
|
|
9115
|
+
else unpostedCount += 1;
|
|
9116
|
+
}
|
|
9117
|
+
return {
|
|
9118
|
+
items: items.filter((e) => states.has(e.id) && (opts?.state === void 0 || states.get(e.id) === opts.state)),
|
|
9119
|
+
postedCount,
|
|
9120
|
+
unpostedCount,
|
|
9121
|
+
window,
|
|
9122
|
+
nextCursor,
|
|
9123
|
+
unconfirmed: items.filter((e) => !states.has(e.id)).length,
|
|
9124
|
+
// A live next-page cursor past the bounded read: the counts above cover
|
|
9125
|
+
// the returned rows ONLY when this is true (issue #66 AC1 — the 500-row cap case is the general case).
|
|
9126
|
+
truncated: hasMore || incomplete
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
9129
|
+
function clampWindowLimit(limit, OP7) {
|
|
9130
|
+
const n = limit === void 0 ? PAGE : Math.trunc(limit);
|
|
9131
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
9132
|
+
throw new AutotaskConfigError(`limit must be an integer >= 1 (got ${String(limit)})`, { operation: OP7 });
|
|
9133
|
+
}
|
|
9134
|
+
return Math.min(n, PAGE);
|
|
9135
|
+
}
|
|
9136
|
+
|
|
9137
|
+
// src/resources/time_entries.ts
|
|
9138
|
+
var TimeEntriesResource = class extends BaseResource {
|
|
9139
|
+
constructor(http, pageSize) {
|
|
9140
|
+
super(http, {
|
|
9141
|
+
key: "time_entries",
|
|
9142
|
+
resourcePath: "/V1.0/TimeEntries",
|
|
9143
|
+
deletable: true,
|
|
9144
|
+
udfCapable: true,
|
|
9145
|
+
collectionUpdate: true
|
|
9146
|
+
// D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
|
|
9147
|
+
}, pageSize);
|
|
9148
|
+
}
|
|
9149
|
+
/** Query TimeEntry records with the G3 filter surface (server-side JSON filter). */
|
|
9150
|
+
query(params) {
|
|
9151
|
+
return this.queryItems(params);
|
|
9152
|
+
}
|
|
9153
|
+
/** Get every TimeEntry record (the MCP-preferred read; 1000-page guarded). */
|
|
9154
|
+
async queryAll(params) {
|
|
9155
|
+
return this.queryAllItems(params);
|
|
9156
|
+
}
|
|
9157
|
+
/** Count matching records (queryCount envelope unwrapped to a plain number). */
|
|
9158
|
+
async count(params) {
|
|
9159
|
+
return this.countGlobal(params);
|
|
9160
|
+
}
|
|
9161
|
+
/** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
|
|
9162
|
+
async get(id, opts = {}) {
|
|
9163
|
+
return this.getGlobal(id, opts?.fresh === true);
|
|
9164
|
+
}
|
|
9165
|
+
/** Create a record (OperationResultModel → { id}). `dryRun: true` returns the
|
|
9166
|
+
* pinned envelope (section 19.3) and never issues the create. A 2xx that is not the
|
|
9167
|
+
* specced envelope runs the plan-pinned marker recovery (issue #13 — one bounded read;
|
|
9168
|
+
* `recoverIdFromMarker: false` restores the pre-recovery behaviour). */
|
|
9169
|
+
async create(data, opts = {}) {
|
|
9170
|
+
if (opts.dryRun === true) return this.runCreateDryRun(data);
|
|
9171
|
+
return this.createGlobal(data, opts);
|
|
9172
|
+
}
|
|
9173
|
+
/** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
|
|
9174
|
+
* keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
|
|
9175
|
+
* partial body is merged into the CURRENT record first: one fresh GET (which also serves the
|
|
9176
|
+
* stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
|
|
9177
|
+
* caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
|
|
9178
|
+
* the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
|
|
9179
|
+
* `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
|
|
9180
|
+
* wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
|
|
9181
|
+
* mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
|
|
9182
|
+
async update(id, data, opts = {}) {
|
|
9183
|
+
if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
|
|
9184
|
+
return this.updateGlobal(id, data, opts);
|
|
9185
|
+
}
|
|
9186
|
+
/** Delete a record (OperationResultModel → { id}). `dryRun: true` returns the pinned
|
|
9187
|
+
* envelope and never issues the delete. */
|
|
9188
|
+
async delete(id, opts = {}) {
|
|
9189
|
+
if (opts.dryRun === true) return this.runDeleteDryRun(id, opts);
|
|
9190
|
+
await this.guardStaleBeforeMutation(id, opts, "time_entries.delete");
|
|
9191
|
+
return this.deleteGlobal(id, opts);
|
|
9192
|
+
}
|
|
9193
|
+
/** Resolve a TimeEntry from a numeric id (or, where the vendor indexes a primary text field,
|
|
9194
|
+
* from a numeric id — this resource has no primary text field). A numeric id miss throws NOT_FOUND;
|
|
9195
|
+
* a bare value rejects with a config error (ids only);
|
|
9196
|
+
* non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
|
|
9197
|
+
* cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
|
|
9198
|
+
* The compact shape is the structural alias of the full record; `expand: true` is the
|
|
9199
|
+
* type-level escape hatch. */
|
|
9200
|
+
async resolve(identifier, opts = {}) {
|
|
9201
|
+
void opts;
|
|
9202
|
+
return this.resolveBase(identifier);
|
|
9203
|
+
}
|
|
9204
|
+
dateWorkedWindow(from, to, opts) {
|
|
9205
|
+
return dateWorkedWindow(this, from, to, opts);
|
|
9206
|
+
}
|
|
9207
|
+
/** Node-parity special op (hand-written in src/resources/special/time_entries/move.ts). */
|
|
9208
|
+
moveToTicket(sourceId, toTicketId, opts = {}) {
|
|
9209
|
+
return moveToTicket2(this, sourceId, toTicketId, opts);
|
|
9210
|
+
}
|
|
9211
|
+
};
|
|
9212
|
+
|
|
8786
9213
|
// src/resources/time_entry_attachments.ts
|
|
8787
9214
|
var TimeEntryAttachmentsResource = class extends BaseResource {
|
|
8788
9215
|
constructor(http, pageSize) {
|
|
@@ -8846,7 +9273,8 @@ var TimeEntryAttachmentsResource = class extends BaseResource {
|
|
|
8846
9273
|
};
|
|
8847
9274
|
|
|
8848
9275
|
// src/resources/special/time_off_requests.ts
|
|
8849
|
-
async function approve(_resource, id, opts = {}
|
|
9276
|
+
async function approve(_resource, id, opts = {}) {
|
|
9277
|
+
const http = _resource.http;
|
|
8850
9278
|
const numericId = parseIdentifier(id, "id");
|
|
8851
9279
|
if (opts.dryRun === true) {
|
|
8852
9280
|
return buildDryRunResult({
|
|
@@ -8867,7 +9295,8 @@ async function approve(_resource, id, opts = {}, http) {
|
|
|
8867
9295
|
});
|
|
8868
9296
|
return unwrapOperation(res);
|
|
8869
9297
|
}
|
|
8870
|
-
async function reject(_resource, id, reason, opts = {}
|
|
9298
|
+
async function reject(_resource, id, reason, opts = {}) {
|
|
9299
|
+
const http = _resource.http;
|
|
8871
9300
|
const numericId = parseIdentifier(id, "id");
|
|
8872
9301
|
const text = reason.trim();
|
|
8873
9302
|
if (text === "") {
|
|
@@ -8934,11 +9363,11 @@ var TimeOffRequestsResource = class extends BaseResource {
|
|
|
8934
9363
|
}
|
|
8935
9364
|
/** Node-parity special op (hand-written in src/resources/special/time_off_requests.ts). */
|
|
8936
9365
|
approve(id, opts = {}) {
|
|
8937
|
-
return approve(this, id, opts
|
|
9366
|
+
return approve(this, id, opts);
|
|
8938
9367
|
}
|
|
8939
9368
|
/** Node-parity special op (hand-written in src/resources/special/time_off_requests.ts). */
|
|
8940
9369
|
reject(id, reason, opts = {}) {
|
|
8941
|
-
return reject(this, id, reason, opts
|
|
9370
|
+
return reject(this, id, reason, opts);
|
|
8942
9371
|
}
|
|
8943
9372
|
};
|
|
8944
9373
|
|
|
@@ -9005,6 +9434,7 @@ var ZoneInformationResource = class {
|
|
|
9005
9434
|
export {
|
|
9006
9435
|
AppointmentsResource,
|
|
9007
9436
|
AuthenticateResource,
|
|
9437
|
+
BillingCodesResource,
|
|
9008
9438
|
BillingItemApprovalLevelsResource,
|
|
9009
9439
|
ChangeRequestLinksResource,
|
|
9010
9440
|
ChecklistLibrariesResource,
|
|
@@ -9084,6 +9514,7 @@ export {
|
|
|
9084
9514
|
ResourceRoleQueuesResource,
|
|
9085
9515
|
ResourceRolesResource,
|
|
9086
9516
|
ResourceSkillsResource,
|
|
9517
|
+
ResourcesResource,
|
|
9087
9518
|
ResourceTimeOffAdditionalResource,
|
|
9088
9519
|
ResourceTimeOffApproversResource,
|
|
9089
9520
|
ResourceTimeOffBalancesResource,
|
|
@@ -9124,6 +9555,7 @@ export {
|
|
|
9124
9555
|
TicketSecondaryResourcesResource,
|
|
9125
9556
|
TicketsResource,
|
|
9126
9557
|
TicketWebhooksResource,
|
|
9558
|
+
TimeEntriesResource,
|
|
9127
9559
|
TimeEntryAttachmentsResource,
|
|
9128
9560
|
TimeOffRequestsResource,
|
|
9129
9561
|
VersionInformationResource,
|