node-autotask 0.16.0 → 0.17.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/N8N_MCP_MANIFEST.json +2 -2
- package/N8N_MCP_MANIFEST.md +3 -3
- package/README.md +36 -0
- package/capabilities.json +4 -4
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-MAYYTYV5.cjs → chunk-4RJG73GY.cjs} +37 -17
- package/dist/{chunk-EWMG3HHU.cjs → chunk-5ZGXWS4E.cjs} +3 -3
- package/dist/{chunk-JIDR3NPV.js → chunk-6SP6GJJR.js} +3 -3
- package/dist/{chunk-VCGC6YCI.cjs → chunk-DQIZUZWP.cjs} +135 -135
- package/dist/{chunk-NB254T3F.js → chunk-EGR2ZLYM.js} +49 -39
- package/dist/{chunk-U6JUHHMB.js → chunk-HC6YWPIY.js} +36 -16
- package/dist/{chunk-L3TK4CN5.cjs → chunk-NQVD2XLH.cjs} +429 -419
- package/dist/{chunk-GWALLMAM.js → chunk-UV2PECAL.js} +2 -2
- package/dist/{index-CUsvZndu.d.ts → index-BAFdPxjG.d.ts} +56 -7
- package/dist/{index-leMyBJze.d.cts → index-CjC0oWZX.d.cts} +56 -7
- package/dist/index.cjs +22 -22
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/operations/index.cjs +103 -103
- package/dist/operations/index.d.cts +12 -6
- package/dist/operations/index.d.ts +12 -6
- package/dist/operations/index.js +2 -2
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/docs/API.md +34 -7
- package/examples/basic.ts +2 -2
- package/examples/mcp-server-http.ts +5 -4
- package/examples/mcp-server.ts +3 -3
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
decodeFileData,
|
|
25
25
|
formatVendorDetail,
|
|
26
26
|
isAutotaskError,
|
|
27
|
+
mergeDetails,
|
|
27
28
|
mergePartialBody,
|
|
28
29
|
parseIdentifier,
|
|
29
30
|
parseVendorErrors,
|
|
@@ -33,10 +34,10 @@ import {
|
|
|
33
34
|
unwrapItem,
|
|
34
35
|
unwrapItems,
|
|
35
36
|
unwrapOperation
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-HC6YWPIY.js";
|
|
37
38
|
import {
|
|
38
39
|
CAPABILITIES
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-6SP6GJJR.js";
|
|
40
41
|
|
|
41
42
|
// src/resources/appointments.ts
|
|
42
43
|
var AppointmentsResource = class extends BaseResource {
|
|
@@ -1102,13 +1103,13 @@ async function upsertByName(resource, companyName, params, opts, http) {
|
|
|
1102
1103
|
if (ownerBody === null || !("item" in ownerBody) || ownerBody.item === null || ownerBody.item === void 0) {
|
|
1103
1104
|
throw new CompanyResourceInvalidError(
|
|
1104
1105
|
`upsertByName: owner resource ${owner} was not found (resources.get returned item: null \u2014 the resources table is blind to this API user; get is the authority)`,
|
|
1105
|
-
{ resourceIds: [owner], operation: OP_UPSERT_NAME, category: "not_found", suggestedAction: "Pass a known active non-API-only resource id (e.g. a ticket assignedResourceID via tickets.fullContext)." }
|
|
1106
|
+
{ resourceIds: [owner], details: { idRefs: [{ entity: "resource", id: owner }] }, operation: OP_UPSERT_NAME, category: "not_found", suggestedAction: "Pass a known active non-API-only resource id (e.g. a ticket assignedResourceID via tickets.fullContext)." }
|
|
1106
1107
|
);
|
|
1107
1108
|
}
|
|
1108
1109
|
if (ownerBody.item.isActive !== true) {
|
|
1109
1110
|
throw new CompanyResourceInvalidError(
|
|
1110
1111
|
`upsertByName: owner resource ${owner} is not active (isActive !== true) \u2014 the vendor requires an active NON-API-only owner`,
|
|
1111
|
-
{ resourceIds: [owner], operation: OP_UPSERT_NAME, suggestedAction: "Activate the resource or pass an active non-API-only one." }
|
|
1112
|
+
{ resourceIds: [owner], details: { idRefs: [{ entity: "resource", id: owner }] }, operation: OP_UPSERT_NAME, suggestedAction: "Activate the resource or pass an active non-API-only one." }
|
|
1112
1113
|
);
|
|
1113
1114
|
}
|
|
1114
1115
|
let merges = 0;
|
|
@@ -1145,7 +1146,8 @@ async function upsertByName(resource, companyName, params, opts, http) {
|
|
|
1145
1146
|
operation: OP_UPSERT_NAME,
|
|
1146
1147
|
httpStatus: err.httpStatus,
|
|
1147
1148
|
vendorError: err.vendorError,
|
|
1148
|
-
resourceIds: [owner]
|
|
1149
|
+
resourceIds: [owner],
|
|
1150
|
+
details: { idRefs: [{ entity: "resource", id: owner }] }
|
|
1149
1151
|
}
|
|
1150
1152
|
);
|
|
1151
1153
|
}
|
|
@@ -2443,7 +2445,7 @@ async function moveConfigurationItem(_resource, sourceId, opts, http) {
|
|
|
2443
2445
|
if (current !== opts.lastModifiedDate) {
|
|
2444
2446
|
throw new StaleObjectError(
|
|
2445
2447
|
`moveConfigurationItem: source item ${numericSource} was modified since the snapshot (expected lastModifiedDate ${opts.lastModifiedDate}, current ${current ?? "unknown"})`,
|
|
2446
|
-
{ operation: "configuration_items.moveConfigurationItem", resourceIds: [numericSource] }
|
|
2448
|
+
{ operation: "configuration_items.moveConfigurationItem", resourceIds: [numericSource], details: { idRefs: [{ entity: "configuration_item", id: numericSource }] } }
|
|
2447
2449
|
);
|
|
2448
2450
|
}
|
|
2449
2451
|
}
|
|
@@ -2496,7 +2498,7 @@ function profilePartLimit2(value, name) {
|
|
|
2496
2498
|
}
|
|
2497
2499
|
return n;
|
|
2498
2500
|
}
|
|
2499
|
-
async function contextFetchPart(http, entityPath, referenceId) {
|
|
2501
|
+
async function contextFetchPart(http, entityPath, referenceId, entity) {
|
|
2500
2502
|
if (typeof referenceId !== "number" || !Number.isSafeInteger(referenceId) || referenceId < 0) {
|
|
2501
2503
|
return null;
|
|
2502
2504
|
}
|
|
@@ -2508,10 +2510,10 @@ async function contextFetchPart(http, entityPath, referenceId) {
|
|
|
2508
2510
|
});
|
|
2509
2511
|
const body = res;
|
|
2510
2512
|
if (body === null || !("item" in body)) {
|
|
2511
|
-
throw new NotFoundError(`context part not found (unexpected envelope shape)`, { resourceIds: [referenceId] });
|
|
2513
|
+
throw new NotFoundError(`context part not found (unexpected envelope shape)`, { resourceIds: [referenceId], details: { idRefs: [{ entity, id: referenceId }] } });
|
|
2512
2514
|
}
|
|
2513
2515
|
if (body.item === null || body.item === void 0) {
|
|
2514
|
-
throw new NotFoundError("context part not found", { resourceIds: [referenceId] });
|
|
2516
|
+
throw new NotFoundError("context part not found", { resourceIds: [referenceId], details: { idRefs: [{ entity, id: referenceId }] } });
|
|
2515
2517
|
}
|
|
2516
2518
|
return body.item;
|
|
2517
2519
|
} catch (err) {
|
|
@@ -2534,8 +2536,8 @@ async function profile(resource, id, opts = {}, http) {
|
|
|
2534
2536
|
const relatedLimit = profilePartLimit2(opts.relatedLimit, "relatedLimit");
|
|
2535
2537
|
const ci = await resource.get(id);
|
|
2536
2538
|
const root = ci;
|
|
2537
|
-
const type = await contextFetchPart(http, "/V1.0/ConfigurationItemTypes", root["configurationItemType"]);
|
|
2538
|
-
const category = await contextFetchPart(http, "/V1.0/ConfigurationItemCategories", root["configurationItemCategoryID"]);
|
|
2539
|
+
const type = await contextFetchPart(http, "/V1.0/ConfigurationItemTypes", root["configurationItemType"], "configuration_item_type");
|
|
2540
|
+
const category = await contextFetchPart(http, "/V1.0/ConfigurationItemCategories", root["configurationItemCategoryID"], "configuration_item_category");
|
|
2539
2541
|
const notes = await ciScopedQuery(http, "/V1.0/ConfigurationItemNotes/query", id, noteLimit);
|
|
2540
2542
|
const relatedItems = await ciScopedQuery(http, "/V1.0/ConfigurationItemRelatedItems/query", id, relatedLimit);
|
|
2541
2543
|
const associations = await ciScopedQuery(http, "/V1.0/ConfigurationItemBillingProductAssociations/query", id, PROFILE_PART_LIMIT2);
|
|
@@ -3026,7 +3028,7 @@ async function moveToCompany(_resource, sourceContactId, destinationCompanyId, o
|
|
|
3026
3028
|
if (current !== opts.lastModifiedDate) {
|
|
3027
3029
|
throw new StaleObjectError(
|
|
3028
3030
|
`moveToCompany: source contact ${sourceId} was modified since the snapshot (expected lastModifiedDate ${opts.lastModifiedDate}, current ${typeof current === "string" ? current : "unknown"})`,
|
|
3029
|
-
{ operation: "contacts.moveToCompany", resourceIds: [sourceId, destId] }
|
|
3031
|
+
{ operation: "contacts.moveToCompany", resourceIds: [sourceId, destId], details: { idRefs: [{ entity: "contact", id: sourceId }, { entity: "company", id: destId }] } }
|
|
3030
3032
|
);
|
|
3031
3033
|
}
|
|
3032
3034
|
}
|
|
@@ -3051,7 +3053,7 @@ async function moveToCompany(_resource, sourceContactId, destinationCompanyId, o
|
|
|
3051
3053
|
if (clash !== void 0) {
|
|
3052
3054
|
throw new ConflictError(
|
|
3053
3055
|
`Contact email "${email}" already exists under company ${destId} (contact ${String(clash.id)}); aborting the move`,
|
|
3054
|
-
{ resourceIds: [sourceId, destId] }
|
|
3056
|
+
{ resourceIds: [sourceId, destId], details: { idRefs: [{ entity: "contact", id: sourceId }, { entity: "company", id: destId }] } }
|
|
3055
3057
|
);
|
|
3056
3058
|
}
|
|
3057
3059
|
}
|
|
@@ -3155,7 +3157,7 @@ async function upsertByEmail(resource, companyID, email, extra, opts, http) {
|
|
|
3155
3157
|
if (parentBody === null || !("item" in parentBody) || parentBody.item === null || parentBody.item === void 0) {
|
|
3156
3158
|
throw new CompanyNotFoundError(
|
|
3157
3159
|
`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)`,
|
|
3158
|
-
{ resourceIds: [cid], operation: OP_UPSERT_EMAIL }
|
|
3160
|
+
{ resourceIds: [cid], details: { idRefs: [{ entity: "company", id: cid }] }, operation: OP_UPSERT_EMAIL }
|
|
3159
3161
|
);
|
|
3160
3162
|
}
|
|
3161
3163
|
const q = await resource.queryBounded({
|
|
@@ -3244,7 +3246,8 @@ async function upsertByEmail(resource, companyID, email, extra, opts, http) {
|
|
|
3244
3246
|
operation: OP_UPSERT_EMAIL,
|
|
3245
3247
|
httpStatus: err.httpStatus,
|
|
3246
3248
|
vendorError: err.vendorError,
|
|
3247
|
-
resourceIds: [cid]
|
|
3249
|
+
resourceIds: [cid],
|
|
3250
|
+
details: { idRefs: [{ entity: "company", id: cid }] }
|
|
3248
3251
|
}
|
|
3249
3252
|
);
|
|
3250
3253
|
}
|
|
@@ -4814,7 +4817,7 @@ function capJoinSet2(ids, cap) {
|
|
|
4814
4817
|
if (sorted.length <= cap) return { ids: sorted, truncated: false, total: sorted.length };
|
|
4815
4818
|
return { ids: sorted.slice(0, cap), truncated: true, total: sorted.length };
|
|
4816
4819
|
}
|
|
4817
|
-
async function fetchByIdsPart(http, entityPath, ids, fields, operation) {
|
|
4820
|
+
async function fetchByIdsPart(http, entityPath, ids, fields, operation, entity) {
|
|
4818
4821
|
const unique = [...new Set(ids)].sort((a, b) => a - b);
|
|
4819
4822
|
const byKey = /* @__PURE__ */ new Map();
|
|
4820
4823
|
const misses = [];
|
|
@@ -4845,7 +4848,7 @@ async function fetchByIdsPart(http, entityPath, ids, fields, operation) {
|
|
|
4845
4848
|
for (const k of queryMissed) {
|
|
4846
4849
|
try {
|
|
4847
4850
|
const res = await http.request({ method: "GET", path: `${entityPath}/${k}`, operation });
|
|
4848
|
-
byKey.set(k, unwrapItem(res, k, operation));
|
|
4851
|
+
byKey.set(k, unwrapItem(res, k, operation, entity));
|
|
4849
4852
|
} catch (err) {
|
|
4850
4853
|
if (err instanceof NotFoundError) {
|
|
4851
4854
|
misses.push({ id: k, code: "NOT_FOUND", message: err.message });
|
|
@@ -5051,7 +5054,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
|
|
|
5051
5054
|
path: `/V1.0/Companies/${companyID}`,
|
|
5052
5055
|
operation: OP2
|
|
5053
5056
|
});
|
|
5054
|
-
const item = unwrapItem(res, companyID, OP2);
|
|
5057
|
+
const item = unwrapItem(res, companyID, OP2, "company");
|
|
5055
5058
|
customer = item ?? null;
|
|
5056
5059
|
customerStatus = "ok";
|
|
5057
5060
|
} catch (err) {
|
|
@@ -5118,7 +5121,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
|
|
|
5118
5121
|
} else {
|
|
5119
5122
|
const capped = capJoinSet2(invoiceIds, FETCH_BY_IDS_MAX_ITEMS);
|
|
5120
5123
|
if (capped.truncated) wires.note = (wires.note ? wires.note + " | " : "") + `invoiceJoinTruncated: the join set is capped at ${FETCH_BY_IDS_MAX_ITEMS} of ${capped.total} invoice ids (the fetch-by-ids cap) \u2014 the figures cover the returned set only`;
|
|
5121
|
-
const fb = await fetchByIdsPart(http, "/V1.0/Invoices", capped.ids, ["id", "invoiceNumber", "invoiceTotal", "paidDate", "isVoided", "dueDate"], OP2);
|
|
5124
|
+
const fb = await fetchByIdsPart(http, "/V1.0/Invoices", capped.ids, ["id", "invoiceNumber", "invoiceTotal", "paidDate", "isVoided", "dueDate"], OP2, "invoice");
|
|
5122
5125
|
wires.issued += 1;
|
|
5123
5126
|
invoices = fb.items;
|
|
5124
5127
|
invoicesPart = { items: fb.items, capped: capped.truncated, hasMore: capped.truncated, status: "ok", note: fb.notes.join(" | ") || void 0 };
|
|
@@ -5170,7 +5173,7 @@ async function profitability(resource, contractId, opts, rawHttp) {
|
|
|
5170
5173
|
} else {
|
|
5171
5174
|
const capped = capJoinSet2(resourceIds, FETCH_BY_IDS_MAX_ITEMS);
|
|
5172
5175
|
if (capped.truncated) wires.note = (wires.note ? wires.note + " | " : "") + `resourceJoinTruncated: the join set is capped at ${FETCH_BY_IDS_MAX_ITEMS} of ${capped.total} resource ids (the fetch-by-ids cap) \u2014 the figures cover the returned set only`;
|
|
5173
|
-
const fb = await fetchByIdsPart(http, "/V1.0/Resources", capped.ids, ["id", "internalCost", "firstName", "lastName", "isActive", "userType"], OP2);
|
|
5176
|
+
const fb = await fetchByIdsPart(http, "/V1.0/Resources", capped.ids, ["id", "internalCost", "firstName", "lastName", "isActive", "userType"], OP2, "resource");
|
|
5174
5177
|
wires.issued += 1;
|
|
5175
5178
|
resources = fb.items;
|
|
5176
5179
|
for (const m of fb.misses) if (m.code === "NOT_FOUND") resourceMissIds.add(m.id);
|
|
@@ -6583,7 +6586,7 @@ async function markup(_resource, id, format, http) {
|
|
|
6583
6586
|
if (body === null || typeof body !== "object" || typeof body.invoiceMarkup !== "string") {
|
|
6584
6587
|
throw new AutotaskError(
|
|
6585
6588
|
`Expected the InvoiceMarkupResultModel envelope { invoiceMarkup: string } for invoice ${numericId}; got a different shape`,
|
|
6586
|
-
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numericId] }
|
|
6589
|
+
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numericId], details: { idRefs: [{ entity: "invoice", id: numericId }] } }
|
|
6587
6590
|
);
|
|
6588
6591
|
}
|
|
6589
6592
|
return body.invoiceMarkup;
|
|
@@ -8044,7 +8047,7 @@ async function whoAmI(_resource, userName, http) {
|
|
|
8044
8047
|
const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
|
|
8045
8048
|
throw new ResolutionError(
|
|
8046
8049
|
`whoAmI: "${value}" is ambiguous (matched ${matches.length} resources)`,
|
|
8047
|
-
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids }
|
|
8050
|
+
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: "resource", id: cid })) } }
|
|
8048
8051
|
);
|
|
8049
8052
|
}
|
|
8050
8053
|
return matches[0];
|
|
@@ -8066,7 +8069,7 @@ async function transferOwnership(_resource, companyIds, destinationResourceId, h
|
|
|
8066
8069
|
operation: "resources.transferOwnership",
|
|
8067
8070
|
maxRetries: opts?.maxRetries
|
|
8068
8071
|
});
|
|
8069
|
-
const company = unwrapItem(res, id);
|
|
8072
|
+
const company = unwrapItem(res, id, void 0, "company");
|
|
8070
8073
|
const current = typeof company.ownerResourceID === "number" ? company.ownerResourceID : null;
|
|
8071
8074
|
changes.push({ id, fromOwnerResourceID: current, toOwnerResourceID: destination });
|
|
8072
8075
|
if (!dryRun) {
|
|
@@ -8083,6 +8086,7 @@ async function transferOwnership(_resource, companyIds, destinationResourceId, h
|
|
|
8083
8086
|
} catch (err) {
|
|
8084
8087
|
if (!dryRun && err instanceof AutotaskError) {
|
|
8085
8088
|
err.resourceIds = [id];
|
|
8089
|
+
err.details = mergeDetails(err.details, { idRefs: [{ entity: "company", id }] });
|
|
8086
8090
|
err.suggestedAction = `Partial transfer: ${applied.length} of ${ids.length} companies were re-owned (ids: ${applied.join(", ") || "none"}); id ${id} failed. Re-run transferOwnership for the remaining ids (from ${id} on).`;
|
|
8087
8091
|
}
|
|
8088
8092
|
throw err;
|
|
@@ -8306,7 +8310,7 @@ async function getByYear(_resource, resourceId, year, http) {
|
|
|
8306
8310
|
path: `/V1.0/Resources/${numericResource}/TimeOffBalances/${year}`,
|
|
8307
8311
|
operation: "resource_time_off_balances.getByYear"
|
|
8308
8312
|
});
|
|
8309
|
-
return unwrapItem(res, year, "resource_time_off_balances.getByYear");
|
|
8313
|
+
return unwrapItem(res, year, "resource_time_off_balances.getByYear", "resource_time_off_balance");
|
|
8310
8314
|
}
|
|
8311
8315
|
|
|
8312
8316
|
// src/resources/resource_time_off_balances.ts
|
|
@@ -10228,9 +10232,9 @@ async function fetchTicket(http, id, operation) {
|
|
|
10228
10232
|
});
|
|
10229
10233
|
const body = res;
|
|
10230
10234
|
if (body === null || !("item" in body)) {
|
|
10231
|
-
throw new NotFoundError(`Ticket ${id} not found (unexpected envelope shape)`, { resourceIds: [id] });
|
|
10235
|
+
throw new NotFoundError(`Ticket ${id} not found (unexpected envelope shape)`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
|
|
10232
10236
|
}
|
|
10233
|
-
if (body.item === null || body.item === void 0) throw new NotFoundError(`Ticket ${id} not found`, { resourceIds: [id] });
|
|
10237
|
+
if (body.item === null || body.item === void 0) throw new NotFoundError(`Ticket ${id} not found`, { resourceIds: [id], details: { idRefs: [{ entity: "ticket", id }] } });
|
|
10234
10238
|
return body.item;
|
|
10235
10239
|
}
|
|
10236
10240
|
async function ticketQueryItems(http, path, filter, maxRecords, operation) {
|
|
@@ -10611,13 +10615,13 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
|
|
|
10611
10615
|
if (rec === null) {
|
|
10612
10616
|
throw new TicketResourceInvalidError(
|
|
10613
10617
|
`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)`,
|
|
10614
|
-
{ resourceIds: [rid], operation: OP_OPEN, category: "not_found", suggestedAction: "Resolve the resource id (a ticket assignedResourceID via tickets.fullContext) and retry." }
|
|
10618
|
+
{ 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." }
|
|
10615
10619
|
);
|
|
10616
10620
|
}
|
|
10617
10621
|
if (rec.isActive !== true) {
|
|
10618
10622
|
throw new TicketResourceInvalidError(
|
|
10619
10623
|
`openWithNoteAndAssign: assignee resource ${assignee} is not active (isActive !== true) \u2014 the vendor would reject the assignment`,
|
|
10620
|
-
{ resourceIds: [rid], operation: OP_OPEN, suggestedAction: "Activate the resource or assign an active one." }
|
|
10624
|
+
{ resourceIds: [rid], details: { idRefs: [{ entity: "resource", id: rid }] }, operation: OP_OPEN, suggestedAction: "Activate the resource or assign an active one." }
|
|
10621
10625
|
);
|
|
10622
10626
|
}
|
|
10623
10627
|
}
|
|
@@ -10698,7 +10702,8 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
|
|
|
10698
10702
|
operation: OP_OPEN,
|
|
10699
10703
|
httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
|
|
10700
10704
|
vendorError: isAutotaskError(err) ? err.vendorError : void 0,
|
|
10701
|
-
resourceIds: [companyID]
|
|
10705
|
+
resourceIds: [companyID],
|
|
10706
|
+
details: { idRefs: [{ entity: "company", id: companyID }] }
|
|
10702
10707
|
}
|
|
10703
10708
|
);
|
|
10704
10709
|
}
|
|
@@ -10719,7 +10724,7 @@ async function openWithNoteAndAssign(resource, params, note, assign, opts, http)
|
|
|
10719
10724
|
try {
|
|
10720
10725
|
const current = await fetchItemUnredacted(http, `/V1.0/Tickets/${ticketId}`, OP_OPEN, { maxRetries: opts.maxRetries });
|
|
10721
10726
|
if (current === null) {
|
|
10722
|
-
throw new NotFoundError(`openWithNoteAndAssign: ticket ${ticketId} vanished between the create and the assign merge read`, { operation: OP_OPEN, resourceIds: [ticketId] });
|
|
10727
|
+
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 }] } });
|
|
10723
10728
|
}
|
|
10724
10729
|
ticketNumber = typeof current.ticketNumber === "string" ? current.ticketNumber : null;
|
|
10725
10730
|
const merged = mergePartialBody(current, { id: ticketId, assignedResourceID: assignee, assignedResourceRoleID: roleID });
|
|
@@ -10826,7 +10831,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
10826
10831
|
}
|
|
10827
10832
|
const current = await fetchItemUnredacted(http, `/V1.0/Tickets/${ticketId}`, OP_STATUS, { maxRetries: opts.maxRetries });
|
|
10828
10833
|
if (current === null) {
|
|
10829
|
-
throw new NotFoundError(`changeStatus: ticket ${ticketId} was not found (tickets.get returned item: null)`, { operation: OP_STATUS, resourceIds: [ticketId] });
|
|
10834
|
+
throw new NotFoundError(`changeStatus: ticket ${ticketId} was not found (tickets.get returned item: null)`, { operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } });
|
|
10830
10835
|
}
|
|
10831
10836
|
const previousStatus = typeof current.status === "number" ? current.status : null;
|
|
10832
10837
|
if (previousStatus === targetStatus) {
|
|
@@ -10869,7 +10874,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
10869
10874
|
throw new StatusFieldsRequiredError(
|
|
10870
10875
|
`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`,
|
|
10871
10876
|
{ vendorFields: [field], currentStatus: previousStatus, targetStatus, currentValues: { [field]: current[field] ?? null } },
|
|
10872
|
-
{ operation: OP_STATUS, resourceIds: [ticketId] }
|
|
10877
|
+
{ operation: OP_STATUS, resourceIds: [ticketId], details: { idRefs: [{ entity: "ticket", id: ticketId }] } }
|
|
10873
10878
|
);
|
|
10874
10879
|
}
|
|
10875
10880
|
mergeData[field] = safe;
|
|
@@ -10930,7 +10935,8 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
10930
10935
|
operation: OP_STATUS,
|
|
10931
10936
|
httpStatus: isAutotaskError(err) ? err.httpStatus : void 0,
|
|
10932
10937
|
vendorError: isAutotaskError(err) ? err.vendorError : void 0,
|
|
10933
|
-
resourceIds: [ticketId]
|
|
10938
|
+
resourceIds: [ticketId],
|
|
10939
|
+
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
10934
10940
|
}
|
|
10935
10941
|
);
|
|
10936
10942
|
}
|
|
@@ -10943,7 +10949,8 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
10943
10949
|
code: "INVALID_RESPONSE",
|
|
10944
10950
|
category: "server",
|
|
10945
10951
|
operation: OP_STATUS,
|
|
10946
|
-
resourceIds: [ticketId]
|
|
10952
|
+
resourceIds: [ticketId],
|
|
10953
|
+
details: { idRefs: [{ entity: "ticket", id: ticketId }] }
|
|
10947
10954
|
});
|
|
10948
10955
|
}
|
|
10949
10956
|
const finalStatus = typeof verify.status === "number" ? verify.status : null;
|
|
@@ -10956,6 +10963,7 @@ async function changeStatus(resource, id, targetStatus, fields, opts, http) {
|
|
|
10956
10963
|
httpStatus: 200,
|
|
10957
10964
|
operation: OP_STATUS,
|
|
10958
10965
|
resourceIds: [ticketId],
|
|
10966
|
+
details: { idRefs: [{ entity: "ticket", id: ticketId }] },
|
|
10959
10967
|
suggestedAction: "Re-run changeStatus (the merge re-reads the current record) or inspect the ticket (tickets.get)."
|
|
10960
10968
|
}
|
|
10961
10969
|
);
|
|
@@ -11405,7 +11413,7 @@ var NoApplicableAllocationCodeError = class extends AutotaskError {
|
|
|
11405
11413
|
}
|
|
11406
11414
|
);
|
|
11407
11415
|
this.name = "NoApplicableAllocationCodeError";
|
|
11408
|
-
this.details = details;
|
|
11416
|
+
this.details = { ...details, idRefs: [{ entity: "resource", id: details.resourceID }] };
|
|
11409
11417
|
}
|
|
11410
11418
|
};
|
|
11411
11419
|
async function boundedQuery(http, path, filter, totalCap, operation) {
|
|
@@ -11489,21 +11497,21 @@ async function resolveCode(http, family, familyLabel, pinnedId, departments, ope
|
|
|
11489
11497
|
throw new AllocationCodeError(
|
|
11490
11498
|
"NOT_FOUND",
|
|
11491
11499
|
`Pinned allocation code ${pinnedId} was not found (billing_codes.get returned item: null)`,
|
|
11492
|
-
{ resourceIds: [pinnedId], operation, suggestedAction: `Resolve an existing ${familyLabel} allocation code id (billing_codes.query with a filter) and re-pin.` }
|
|
11500
|
+
{ resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: `Resolve an existing ${familyLabel} allocation code id (billing_codes.query with a filter) and re-pin.` }
|
|
11493
11501
|
);
|
|
11494
11502
|
}
|
|
11495
11503
|
if (rec.isActive !== true) {
|
|
11496
11504
|
throw new AllocationCodeError(
|
|
11497
11505
|
"INACTIVE",
|
|
11498
11506
|
`Pinned allocation code ${pinnedId} (${typeof rec.name === "string" ? rec.name : "unknown name"}) is not active (isActive !== true)`,
|
|
11499
|
-
{ resourceIds: [pinnedId], operation, suggestedAction: "Activate the code in the tenant administration or pin an active one." }
|
|
11507
|
+
{ resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: "Activate the code in the tenant administration or pin an active one." }
|
|
11500
11508
|
);
|
|
11501
11509
|
}
|
|
11502
11510
|
if (rec.useType !== family) {
|
|
11503
11511
|
throw new AllocationCodeError(
|
|
11504
11512
|
"WRONG_TYPE",
|
|
11505
11513
|
`Pinned allocation code ${pinnedId} is useType ${String(rec.useType ?? "unknown")}, not the ${familyLabel} family (useType ${family})`,
|
|
11506
|
-
{ resourceIds: [pinnedId], operation, suggestedAction: `Pin a code from the right family (billing_codes.query with useType eq ${family} + isActive eq true).` }
|
|
11514
|
+
{ resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: `Pin a code from the right family (billing_codes.query with useType eq ${family} + isActive eq true).` }
|
|
11507
11515
|
);
|
|
11508
11516
|
}
|
|
11509
11517
|
const dept = typeof rec.department === "number" ? rec.department : null;
|
|
@@ -11511,7 +11519,7 @@ async function resolveCode(http, family, familyLabel, pinnedId, departments, ope
|
|
|
11511
11519
|
throw new AllocationCodeError(
|
|
11512
11520
|
"NOT_APPLICABLE",
|
|
11513
11521
|
`Pinned allocation code ${pinnedId} is scoped to department ${dept}, which is not among the resource's active role departments [${[...departments].sort((a, b) => a - b).join(", ")}] (and the code is not tenant-wide)`,
|
|
11514
|
-
{ resourceIds: [pinnedId], operation, suggestedAction: "Pin a tenant-wide (department null) code or one scoped to a resource department, or broaden the resource role assignments." }
|
|
11522
|
+
{ resourceIds: [pinnedId], details: { idRefs: [{ entity: "billing_code", id: pinnedId }] }, operation, suggestedAction: "Pin a tenant-wide (department null) code or one scoped to a resource department, or broaden the resource role assignments." }
|
|
11515
11523
|
);
|
|
11516
11524
|
}
|
|
11517
11525
|
return rec;
|
|
@@ -11574,6 +11582,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
|
|
|
11574
11582
|
if (rec === null) {
|
|
11575
11583
|
throw new ResourceInvalidError(`Resource ${rid} was not found \u2014 a time entry cannot attach to a missing resource`, {
|
|
11576
11584
|
resourceIds: [rid],
|
|
11585
|
+
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
11577
11586
|
operation: OP,
|
|
11578
11587
|
category: "not_found",
|
|
11579
11588
|
suggestedAction: "Resolve the resource id (resources.get / resources.search) and retry."
|
|
@@ -11582,6 +11591,7 @@ async function createForResource(resource, resourceID, params, opts = {}, http)
|
|
|
11582
11591
|
if (rec.isActive !== true) {
|
|
11583
11592
|
throw new ResourceInvalidError(`Resource ${rid} is not active (isActive !== true) \u2014 the vendor would reject a time entry for an inactive resource`, {
|
|
11584
11593
|
resourceIds: [rid],
|
|
11594
|
+
details: { idRefs: [{ entity: "resource", id: rid }] },
|
|
11585
11595
|
operation: OP,
|
|
11586
11596
|
suggestedAction: "Activate the resource or pick an active one."
|
|
11587
11597
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CAPABILITIES
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6SP6GJJR.js";
|
|
4
4
|
|
|
5
5
|
// src/auth.ts
|
|
6
6
|
var AUTH_HEADERS = {
|
|
@@ -171,6 +171,7 @@ var AutotaskError = class extends Error {
|
|
|
171
171
|
operation;
|
|
172
172
|
httpStatus;
|
|
173
173
|
vendorError;
|
|
174
|
+
/** Legacy unlabeled id list (issue #116) — see `AutotaskErrorOptions.resourceIds`; the labeled companion rides on `details.idRefs`. */
|
|
174
175
|
resourceIds;
|
|
175
176
|
suggestedAction;
|
|
176
177
|
correlationId;
|
|
@@ -15655,7 +15656,7 @@ function unwrapItems(body, operation) {
|
|
|
15655
15656
|
}
|
|
15656
15657
|
return { items: body.items, pageDetails: body.pageDetails };
|
|
15657
15658
|
}
|
|
15658
|
-
function unwrapItem(body, id, operation) {
|
|
15659
|
+
function unwrapItem(body, id, operation, entity) {
|
|
15659
15660
|
if (!isRecord2(body) || !("item" in body)) {
|
|
15660
15661
|
throw new AutotaskError("Expected the item envelope { item }; got a different shape", {
|
|
15661
15662
|
code: "INVALID_RESPONSE",
|
|
@@ -15671,6 +15672,7 @@ function unwrapItem(body, id, operation) {
|
|
|
15671
15672
|
{
|
|
15672
15673
|
httpStatus: 200,
|
|
15673
15674
|
resourceIds: id !== void 0 ? [id] : void 0,
|
|
15675
|
+
...id !== void 0 && entity !== void 0 ? { details: { idRefs: [{ entity, id }] } } : {},
|
|
15674
15676
|
operation,
|
|
15675
15677
|
suggestedAction: "Verify the id, or resolve the record by name first."
|
|
15676
15678
|
}
|
|
@@ -16991,6 +16993,19 @@ function decodeFileData(body) {
|
|
|
16991
16993
|
{ code: "INVALID_RESPONSE", category: "server" }
|
|
16992
16994
|
);
|
|
16993
16995
|
}
|
|
16996
|
+
var DEPLURAL_EXCEPTIONS = {
|
|
16997
|
+
tag_aliase: "tag_alias"
|
|
16998
|
+
};
|
|
16999
|
+
function entityNameFromPath(resourcePath) {
|
|
17000
|
+
const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
|
|
17001
|
+
const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
|
|
17002
|
+
if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
|
|
17003
|
+
if (snake.endsWith("s")) {
|
|
17004
|
+
const stripped = snake.slice(0, -1);
|
|
17005
|
+
return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
|
|
17006
|
+
}
|
|
17007
|
+
return snake;
|
|
17008
|
+
}
|
|
16994
17009
|
var BaseResource = class {
|
|
16995
17010
|
http;
|
|
16996
17011
|
config;
|
|
@@ -17000,6 +17015,10 @@ var BaseResource = class {
|
|
|
17000
17015
|
get resourcePath() {
|
|
17001
17016
|
return this.config.resourcePath;
|
|
17002
17017
|
}
|
|
17018
|
+
/** The vendor entity this resource's ids identify (issue #116 — the `details.idRefs` label, e.g. `company`). */
|
|
17019
|
+
get entityName() {
|
|
17020
|
+
return entityNameFromPath(this.resourcePath);
|
|
17021
|
+
}
|
|
17003
17022
|
/** Parent-scoped template (from the config); undefined for top-level resources. */
|
|
17004
17023
|
get parentScopedPath() {
|
|
17005
17024
|
return this.config.parentScopedPath;
|
|
@@ -17507,7 +17526,7 @@ var BaseResource = class {
|
|
|
17507
17526
|
async getGlobal(id, fresh = false) {
|
|
17508
17527
|
const numeric = parseIdentifier(id, "id");
|
|
17509
17528
|
const res = await this.cachedGet(`${this.resourcePath}/${numeric}`, `${this.config.key}.get`, fresh, this.config.key);
|
|
17510
|
-
return unwrapItem(res, numeric, `${this.config.key}.get
|
|
17529
|
+
return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
|
|
17511
17530
|
}
|
|
17512
17531
|
/**
|
|
17513
17532
|
* V2 merge read: a FRESH single-record GET that skips response redaction
|
|
@@ -17529,7 +17548,7 @@ var BaseResource = class {
|
|
|
17529
17548
|
skipResponseRedaction: true,
|
|
17530
17549
|
maxRetries: readOpts?.maxRetries
|
|
17531
17550
|
});
|
|
17532
|
-
return unwrapItem(res, numeric, `${this.config.key}.get
|
|
17551
|
+
return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
|
|
17533
17552
|
}
|
|
17534
17553
|
/**
|
|
17535
17554
|
* Issue #34 fallback merge read: a FRESH parent-scoped single-record GET
|
|
@@ -17548,13 +17567,13 @@ var BaseResource = class {
|
|
|
17548
17567
|
skipResponseRedaction: true,
|
|
17549
17568
|
maxRetries: readOpts?.maxRetries
|
|
17550
17569
|
});
|
|
17551
|
-
return unwrapItem(res, numeric, `${this.config.key}.get
|
|
17570
|
+
return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
|
|
17552
17571
|
}
|
|
17553
17572
|
/** Parent-scoped `GET .../{parentId}/<Child>/{id}` → `{ item }` unwrapped. */
|
|
17554
17573
|
async getScopedRaw(parentId, id, fresh = false) {
|
|
17555
17574
|
const numeric = parseIdentifier(id, "id");
|
|
17556
17575
|
const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
|
|
17557
|
-
return unwrapItem(res, numeric, `${this.config.key}.get
|
|
17576
|
+
return unwrapItem(res, numeric, `${this.config.key}.get`, this.entityName);
|
|
17558
17577
|
}
|
|
17559
17578
|
/**
|
|
17560
17579
|
* Attachment single-read: the vendor answers the model DIRECTLY (swagger),
|
|
@@ -17564,23 +17583,23 @@ var BaseResource = class {
|
|
|
17564
17583
|
async getScopedModel(parentId, id, fresh = false) {
|
|
17565
17584
|
const numeric = parseIdentifier(id, "id");
|
|
17566
17585
|
const res = await this.cachedGet(this.scopedItemPath(parentId, id), `${this.config.key}.get`, fresh, this.config.key);
|
|
17567
|
-
return this.unwrapModelLike(res, numeric);
|
|
17586
|
+
return this.unwrapModelLike(res, numeric, this.entityName);
|
|
17568
17587
|
}
|
|
17569
|
-
unwrapModelLike(body, id) {
|
|
17588
|
+
unwrapModelLike(body, id, entity) {
|
|
17570
17589
|
if (!isRecord2(body)) {
|
|
17571
17590
|
throw new AutotaskError(
|
|
17572
17591
|
`Expected an attachment model (or { item } / { items }) envelope for ID ${id}; got a different shape`,
|
|
17573
|
-
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [id] }
|
|
17592
|
+
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [id], details: { idRefs: [{ entity, id }] } }
|
|
17574
17593
|
);
|
|
17575
17594
|
}
|
|
17576
17595
|
if (Object.keys(body).length === 0) {
|
|
17577
|
-
throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id] });
|
|
17596
|
+
throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id], details: { idRefs: [{ entity, id }] } });
|
|
17578
17597
|
}
|
|
17579
|
-
if ("item" in body) return unwrapItem(body, id);
|
|
17598
|
+
if ("item" in body) return unwrapItem(body, id, void 0, entity);
|
|
17580
17599
|
if (Array.isArray(body.items)) {
|
|
17581
17600
|
const first = body.items[0];
|
|
17582
17601
|
if (first === null || first === void 0) {
|
|
17583
|
-
throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id] });
|
|
17602
|
+
throw new NotFoundError(`The record with ID ${id} was not found`, { httpStatus: 200, resourceIds: [id], details: { idRefs: [{ entity, id }] } });
|
|
17584
17603
|
}
|
|
17585
17604
|
return first;
|
|
17586
17605
|
}
|
|
@@ -17928,7 +17947,7 @@ var BaseResource = class {
|
|
|
17928
17947
|
if (!isRecord2(res)) {
|
|
17929
17948
|
throw new AutotaskError(
|
|
17930
17949
|
`Expected the deleted attachment model for ID ${numeric}; got a different shape`,
|
|
17931
|
-
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric] }
|
|
17950
|
+
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric], details: { idRefs: [{ entity: this.entityName, id: numeric }] } }
|
|
17932
17951
|
);
|
|
17933
17952
|
}
|
|
17934
17953
|
return res;
|
|
@@ -18319,9 +18338,9 @@ var BaseResource = class {
|
|
|
18319
18338
|
const resource = RESOURCE_KEY_BY_PATH[entityPath];
|
|
18320
18339
|
const res = await this.cachedGet(`${entityPath}/${numeric}`, `${this.config.key}.fetchRecord`, fresh, resource);
|
|
18321
18340
|
if (this.config.attachmentGet) {
|
|
18322
|
-
return this.unwrapModelLike(res, numeric);
|
|
18341
|
+
return this.unwrapModelLike(res, numeric, entityNameFromPath(entityPath));
|
|
18323
18342
|
}
|
|
18324
|
-
return unwrapItem(res, numeric, `${this.config.key}.fetchRecord
|
|
18343
|
+
return unwrapItem(res, numeric, `${this.config.key}.fetchRecord`, entityNameFromPath(entityPath));
|
|
18325
18344
|
}
|
|
18326
18345
|
/**
|
|
18327
18346
|
* `getContext` part fetch: a single-record read that degrades to `null`
|
|
@@ -18428,6 +18447,7 @@ var BaseResource = class {
|
|
|
18428
18447
|
{
|
|
18429
18448
|
code: "RESOLUTION_AMBIGUOUS",
|
|
18430
18449
|
resourceIds: ids,
|
|
18450
|
+
details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) },
|
|
18431
18451
|
// issue #59 PR-B: the miss-channel pointer (the generic floor names
|
|
18432
18452
|
// the resource's own search op — reachable only where searchFields
|
|
18433
18453
|
// is non-empty, so the named op always exists here).
|
|
@@ -18564,7 +18584,7 @@ var BaseResource = class {
|
|
|
18564
18584
|
const ids = matches.map((m) => m.id).filter((id) => typeof id === "number");
|
|
18565
18585
|
throw new ResolutionError(
|
|
18566
18586
|
`findBy${field}: "${value}" is ambiguous on ${this.config.key} (matched ${matches.length} records)`,
|
|
18567
|
-
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids }
|
|
18587
|
+
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids, details: { idRefs: ids.map((cid) => ({ entity: this.entityName, id: cid })) } }
|
|
18568
18588
|
);
|
|
18569
18589
|
}
|
|
18570
18590
|
return matches[0];
|