node-autotask 0.20.0 → 0.23.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/ARCHITECTURE.md +44 -21
- package/CHANGELOG.md +42 -0
- package/MCP_TOOL_CATALOG.json +1458 -12
- package/MCP_TOOL_MANIFEST.md +23 -20
- package/MCP_TOOL_OVERRIDES.json +7 -7
- package/N8N_MCP_MANIFEST.json +20 -4
- package/N8N_MCP_MANIFEST.md +6 -5
- package/README.md +9 -9
- package/capabilities.json +12362 -7886
- package/capabilities.schema.json +33 -12
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.d.cts +9 -2
- package/dist/capabilities.d.ts +9 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-GA6UCX3Z.js → chunk-B5SNZ3TZ.js} +3507 -3113
- package/dist/{chunk-L6V66V22.cjs → chunk-D5TVBI4U.cjs} +3039 -2645
- package/dist/{chunk-FLYTKJKV.cjs → chunk-JUO3URUI.cjs} +12361 -7885
- package/dist/{chunk-YH5CPRDN.cjs → chunk-N27NUB4R.cjs} +2466 -1745
- package/dist/{chunk-2T77J2ZM.js → chunk-OGNUI3UR.js} +12361 -7885
- package/dist/{chunk-XSYJB4IW.js → chunk-PVUUOM5G.js} +2092 -1371
- package/dist/{chunk-RMLGOCGA.cjs → chunk-TTQB537V.cjs} +135 -135
- package/dist/{chunk-JEJUFXHX.js → chunk-VYFO2N5F.js} +2 -2
- package/dist/{index-BXtKN1M5.d.cts → index-BVtycq-6.d.cts} +1 -1
- package/dist/{index-BXtKN1M5.d.ts → index-BVtycq-6.d.ts} +1 -1
- package/dist/{index-AN-NjLi9.d.cts → index-DBoX-Dnk.d.ts} +430 -952
- package/dist/{index-CvLLsOCP.d.ts → index-NC-x-bYl.d.cts} +430 -952
- package/dist/index.cjs +91 -62
- package/dist/index.d.cts +360 -5
- package/dist/index.d.ts +360 -5
- package/dist/index.js +74 -45
- package/dist/operations/index.cjs +462 -462
- package/dist/operations/index.d.cts +78 -84
- package/dist/operations/index.d.ts +78 -84
- package/dist/operations/index.js +382 -382
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +2 -2
- package/dist/pool.d.ts +2 -2
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +2 -2
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.js +3 -3
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/docs/API.md +140 -45
- package/docs/N8N-ADAPTER.md +3 -3
- package/examples/mcp/errors.ts +135 -0
- package/examples/mcp/manifest.ts +168 -0
- package/examples/mcp/register.ts +66 -0
- package/examples/mcp/schemas.ts +203 -0
- package/examples/mcp/tools-meta.ts +120 -0
- package/examples/mcp/tools-mode.ts +177 -0
- package/examples/mcp/tools-read.ts +264 -0
- package/examples/mcp/types.ts +34 -0
- package/examples/mcp-register.ts +9 -1074
- package/package.json +1 -1
package/dist/operations/index.js
CHANGED
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
RELATIONSHIP_MAP,
|
|
25
25
|
RELATIONSHIP_ZONE,
|
|
26
26
|
RecoveryTruncatedError,
|
|
27
|
+
UI_LINK_ENTITIES,
|
|
27
28
|
WORKFLOW_RESOURCES,
|
|
28
29
|
buildFilter,
|
|
30
|
+
buildUiLink,
|
|
29
31
|
camelKey,
|
|
30
32
|
clampPageSize,
|
|
31
33
|
expandResourceNames,
|
|
@@ -34,13 +36,14 @@ import {
|
|
|
34
36
|
fieldCatalog,
|
|
35
37
|
isAutotaskError,
|
|
36
38
|
isRecord,
|
|
37
|
-
normalizedConditions,
|
|
38
39
|
parseIdentifier,
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
uiLink,
|
|
41
|
+
validateFetchManyItems,
|
|
42
|
+
validateQueryFieldNames
|
|
43
|
+
} from "../chunk-B5SNZ3TZ.js";
|
|
41
44
|
import {
|
|
42
45
|
CAPABILITIES
|
|
43
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-OGNUI3UR.js";
|
|
44
47
|
|
|
45
48
|
// src/operations/searchAcrossResources.ts
|
|
46
49
|
var DEFAULT_LIMIT = 25;
|
|
@@ -57,19 +60,35 @@ async function searchAcrossResources(client, query, opts = {}) {
|
|
|
57
60
|
throw new AutotaskConfigError("searchAcrossResources: no resources requested");
|
|
58
61
|
}
|
|
59
62
|
const limit = clampPageSize(opts.limit ?? DEFAULT_LIMIT);
|
|
63
|
+
const totalLimit = opts.totalLimit;
|
|
64
|
+
if (totalLimit !== void 0 && !(Number.isInteger(totalLimit) && totalLimit >= 1)) {
|
|
65
|
+
throw new AutotaskConfigError("searchAcrossResources: totalLimit must be a positive integer");
|
|
66
|
+
}
|
|
60
67
|
const results = [];
|
|
61
68
|
const skipped = [];
|
|
69
|
+
const notSearched = [];
|
|
70
|
+
let remaining = totalLimit ?? Infinity;
|
|
71
|
+
let hitQuota = false;
|
|
62
72
|
for (const key of keys) {
|
|
63
73
|
const holder = resourceFor(client, key);
|
|
64
74
|
if (holder === void 0 || typeof holder.search !== "function") {
|
|
65
75
|
skipped.push(key);
|
|
66
76
|
continue;
|
|
67
77
|
}
|
|
68
|
-
|
|
78
|
+
if (remaining <= 0) {
|
|
79
|
+
notSearched.push(key);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const ask = Math.min(limit, remaining);
|
|
83
|
+
const matches = await holder.search(text, { limit: ask });
|
|
69
84
|
const rows = Array.isArray(matches) ? matches.filter((m) => typeof m === "object" && m !== null).map((m) => m) : [];
|
|
85
|
+
if (totalLimit !== void 0) rows.length = Math.min(rows.length, ask);
|
|
70
86
|
results.push({ resource: key, matches: rows });
|
|
87
|
+
remaining -= rows.length;
|
|
88
|
+
if (rows.length === ask) hitQuota = true;
|
|
71
89
|
}
|
|
72
|
-
return { query: text, results, skipped };
|
|
90
|
+
if (totalLimit === void 0) return { query: text, results, skipped };
|
|
91
|
+
return { query: text, results, skipped, notSearched, truncated: notSearched.length > 0 || hitQuota };
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
// src/operations/resolveAny.ts
|
|
@@ -213,39 +232,41 @@ var OP_RE = /^([a-z0-9_]+)\.query$/;
|
|
|
213
232
|
function num(v) {
|
|
214
233
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
215
234
|
}
|
|
216
|
-
function
|
|
217
|
-
const problems = [];
|
|
218
|
-
let resourceKey;
|
|
235
|
+
function parseOperationKey(operation, problems) {
|
|
219
236
|
if (typeof operation !== "string" || !OP_RE.test(operation)) {
|
|
220
237
|
problems.push(
|
|
221
238
|
`operation must be exactly "<planResourceKey>.query" (the resource's bounded query op \u2014 e.g. "billing_items.query"); a special op, a ".count" row, or any other verb is not an aggregate surface \u2014 see autotask_catalog for the exact plan keys`
|
|
222
239
|
);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
const key = OP_RE.exec(operation)[1];
|
|
243
|
+
if (!QUERY_RESOURCE_KEYS.has(key)) {
|
|
244
|
+
problems.push(
|
|
245
|
+
`operation "${operation}": "${key}" has no query row in the capability registry (116 query surfaces) \u2014 see autotask_catalog for the exact plan keys`
|
|
246
|
+
);
|
|
231
247
|
}
|
|
232
|
-
|
|
248
|
+
return key;
|
|
249
|
+
}
|
|
250
|
+
function parseFilterConditions(filter, problems) {
|
|
233
251
|
if (filter === void 0 || filter === null) {
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
conditions = [];
|
|
241
|
-
} else {
|
|
242
|
-
try {
|
|
243
|
-
conditions = buildFilter(conds).filter;
|
|
244
|
-
} catch (err) {
|
|
245
|
-
problems.push(err instanceof AutotaskConfigError ? err.message : `filter must be the query-bag filter (13 operators; caps 500 OR-conditions / 1 UDF / 8000 chars); ${String(err)}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
const conds = Array.isArray(filter) ? filter : filter.filter;
|
|
255
|
+
if (!Array.isArray(conds)) {
|
|
256
|
+
problems.push("filter must be the query-bag filter (a conditions array, or { filter: [...] })");
|
|
257
|
+
return [];
|
|
248
258
|
}
|
|
259
|
+
if (conds.length === 0) {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
return buildFilter(conds).filter;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
problems.push(err instanceof AutotaskConfigError ? err.message : `filter must be the query-bag filter (13 operators; caps 500 OR-conditions / 1 UDF / 8000 chars); ${String(err)}`);
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function checkOptsBag(opts, problems) {
|
|
249
270
|
if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
|
|
250
271
|
throw new AutotaskConfigError(
|
|
251
272
|
`aggregate: opts must be the options object (got ${Array.isArray(opts) ? "an array" : typeof opts})`,
|
|
@@ -257,58 +278,63 @@ function validateAggregate(operation, filter, opts) {
|
|
|
257
278
|
problems.push(`unknown opts key "${key}" \u2014 the bag is closed: ${AGGREGATE_OPT_KEYS.join(", ")}`);
|
|
258
279
|
}
|
|
259
280
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
281
|
+
}
|
|
282
|
+
function checkSumFields(name, fields, oracles, problems) {
|
|
283
|
+
if (fields === void 0) return [];
|
|
284
|
+
if (!Array.isArray(fields) || fields.length === 0 || fields.some((f) => typeof f !== "string" || f.trim() === "")) {
|
|
285
|
+
problems.push(`${name} must be a non-empty array of exact vendor field names (discover them via operations.fieldCatalog)`);
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const out = [];
|
|
289
|
+
for (const f of fields) {
|
|
290
|
+
if (oracles.fieldSets === void 0 || !oracles.fieldSets.includes(f)) {
|
|
291
|
+
problems.push(`${name}: "${f}" is not a field of ${oracles.resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
|
|
292
|
+
} else {
|
|
293
|
+
out.push(f);
|
|
267
294
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
function parseGroupSpec(f, oracles, problems) {
|
|
299
|
+
const { resourceKey, fieldSets, dateSets } = oracles;
|
|
300
|
+
const at = f.indexOf("@");
|
|
301
|
+
if (at === -1) {
|
|
302
|
+
if (fieldSets === void 0 || !fieldSets.includes(f)) {
|
|
303
|
+
problems.push(`groupBy: "${f}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
|
|
304
|
+
return void 0;
|
|
275
305
|
}
|
|
276
|
-
return
|
|
277
|
-
}
|
|
306
|
+
return { name: f, base: f };
|
|
307
|
+
}
|
|
308
|
+
const base = f.slice(0, at);
|
|
309
|
+
const suffix = f.slice(at + 1);
|
|
310
|
+
if (!AGGREGATE_DATE_BUCKETS.includes(suffix)) {
|
|
311
|
+
problems.push(`groupBy: "${f}": unknown bucket suffix "${suffix}" \u2014 the closed set is month | week | year (the suffix applies to date-typed fields only)`);
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
if (fieldSets === void 0 || !fieldSets.includes(base)) {
|
|
315
|
+
problems.push(`groupBy: "${f}": "${base}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
if (dateSets === void 0 || !dateSets.includes(base)) {
|
|
319
|
+
problems.push(`groupBy: "${f}": "${base}" is not a date field of ${resourceKey} (the @month/@week/@year bucket suffix applies to date-typed fields of the resource's vendored model \u2014 exact vendor field names via operations.fieldCatalog)`);
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
return { name: f, base, bucket: suffix };
|
|
323
|
+
}
|
|
324
|
+
function parseGroupBy(opts, oracles, problems) {
|
|
278
325
|
const groupBy = [];
|
|
279
|
-
if (opts.groupBy
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
problems.push(`groupBy: "${f}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
|
|
288
|
-
} else {
|
|
289
|
-
groupBy.push({ name: f, base: f });
|
|
290
|
-
}
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
const base = f.slice(0, at);
|
|
294
|
-
const suffix = f.slice(at + 1);
|
|
295
|
-
if (!AGGREGATE_DATE_BUCKETS.includes(suffix)) {
|
|
296
|
-
problems.push(`groupBy: "${f}": unknown bucket suffix "${suffix}" \u2014 the closed set is month | week | year (the suffix applies to date-typed fields only)`);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (fieldSets === void 0 || !fieldSets.includes(base)) {
|
|
300
|
-
problems.push(`groupBy: "${f}": "${base}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (dateSets === void 0 || !dateSets.includes(base)) {
|
|
304
|
-
problems.push(`groupBy: "${f}": "${base}" is not a date field of ${resourceKey} (the @month/@week/@year bucket suffix applies to date-typed fields of the resource's vendored model \u2014 exact vendor field names via operations.fieldCatalog)`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
groupBy.push({ name: f, base, bucket: suffix });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
326
|
+
if (opts.groupBy === void 0) return groupBy;
|
|
327
|
+
if (!Array.isArray(opts.groupBy) || opts.groupBy.length === 0 || opts.groupBy.some((f) => typeof f !== "string" || f.trim() === "")) {
|
|
328
|
+
problems.push("groupBy must be a non-empty array of exact vendor field names (a date-typed field may carry the closed bucket suffix @month | @week | @year \u2014 discover them via operations.fieldCatalog)");
|
|
329
|
+
return groupBy;
|
|
330
|
+
}
|
|
331
|
+
for (const f of opts.groupBy) {
|
|
332
|
+
const spec = parseGroupSpec(f, oracles, problems);
|
|
333
|
+
if (spec !== void 0) groupBy.push(spec);
|
|
310
334
|
}
|
|
311
|
-
|
|
335
|
+
return groupBy;
|
|
336
|
+
}
|
|
337
|
+
function parseTopAndLimit(opts, problems) {
|
|
312
338
|
let top = AGGREGATE_DEFAULT_TOP;
|
|
313
339
|
if (opts.top !== void 0) {
|
|
314
340
|
if (!Number.isInteger(opts.top)) {
|
|
@@ -325,6 +351,21 @@ function validateAggregate(operation, filter, opts) {
|
|
|
325
351
|
limit = Math.min(Math.max(opts.limit, 1), BOUNDED_QUERY_MAX_LIMIT);
|
|
326
352
|
}
|
|
327
353
|
}
|
|
354
|
+
return { top, limit };
|
|
355
|
+
}
|
|
356
|
+
function validateAggregate(operation, filter, opts) {
|
|
357
|
+
const problems = [];
|
|
358
|
+
const resourceKey = parseOperationKey(operation, problems);
|
|
359
|
+
const conditions = parseFilterConditions(filter, problems);
|
|
360
|
+
checkOptsBag(opts, problems);
|
|
361
|
+
const oracles = {
|
|
362
|
+
resourceKey,
|
|
363
|
+
fieldSets: resourceKey !== void 0 ? MODEL_FIELD_SETS[resourceKey] : void 0,
|
|
364
|
+
dateSets: resourceKey !== void 0 ? MODEL_DATE_FIELDS[resourceKey] : void 0
|
|
365
|
+
};
|
|
366
|
+
const groupBy = parseGroupBy(opts, oracles, problems);
|
|
367
|
+
const sum = checkSumFields("sum", opts.sum, oracles, problems);
|
|
368
|
+
const { top, limit } = parseTopAndLimit(opts, problems);
|
|
328
369
|
if (problems.length > 0) {
|
|
329
370
|
throw new AutotaskConfigError(
|
|
330
371
|
`aggregate: ${problems.length} problem(s) found pre-request (zero wire was issued)
|
|
@@ -334,27 +375,9 @@ function validateAggregate(operation, filter, opts) {
|
|
|
334
375
|
}
|
|
335
376
|
return { resourceKey, conditions, groupBy, sum, top, limit };
|
|
336
377
|
}
|
|
337
|
-
|
|
338
|
-
const v = validateAggregate(operation, filter, opts);
|
|
339
|
-
const holder = client[camelKey(v.resourceKey)];
|
|
340
|
-
if (holder === void 0) {
|
|
341
|
-
throw new AutotaskConfigError(`aggregate: the client exposes no accessor for resource "${v.resourceKey}"`, {
|
|
342
|
-
operation: "operations.aggregate",
|
|
343
|
-
suggestedAction: "Rebuild the client from the SDK build that ships this registry (the resource set is pinned by the plan)."
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
const wireFilter = v.conditions.length === 0 ? void 0 : v.conditions;
|
|
347
|
-
if (v.groupBy.length === 0 && v.sum.length === 0) {
|
|
348
|
-
const raw = await holder.count({ filter: wireFilter });
|
|
349
|
-
const sentinel = typeof raw === "object" && raw !== null && "count" in raw ? raw : { count: raw };
|
|
350
|
-
const result2 = { mode: "count", count: sentinel.count, blind: sentinel.blindSurface !== void 0 };
|
|
351
|
-
if (sentinel.blindSurface !== void 0) result2.blindSurface = sentinel.blindSurface;
|
|
352
|
-
if (sentinel.emptyReason !== void 0) result2.emptyReason = sentinel.emptyReason;
|
|
353
|
-
return result2;
|
|
354
|
-
}
|
|
355
|
-
const res = await holder.queryBounded({ filter: wireFilter, limit: v.limit });
|
|
378
|
+
function foldGroups(items, v) {
|
|
356
379
|
const groups = /* @__PURE__ */ new Map();
|
|
357
|
-
for (const item of
|
|
380
|
+
for (const item of items) {
|
|
358
381
|
const key = {};
|
|
359
382
|
const values = [];
|
|
360
383
|
for (const spec of v.groupBy) {
|
|
@@ -374,7 +397,28 @@ async function aggregate(client, operation, filter, opts = {}) {
|
|
|
374
397
|
for (const f of v.sum) g.sums[f] = (g.sums[f] ?? 0) + num(item[f]);
|
|
375
398
|
}
|
|
376
399
|
}
|
|
377
|
-
|
|
400
|
+
return [...groups.values()].sort((a, b) => b.count - a.count || (a.keyOrder < b.keyOrder ? -1 : a.keyOrder > b.keyOrder ? 1 : 0));
|
|
401
|
+
}
|
|
402
|
+
async function aggregate(client, operation, filter, opts = {}) {
|
|
403
|
+
const v = validateAggregate(operation, filter, opts);
|
|
404
|
+
const holder = client[camelKey(v.resourceKey)];
|
|
405
|
+
if (holder === void 0) {
|
|
406
|
+
throw new AutotaskConfigError(`aggregate: the client exposes no accessor for resource "${v.resourceKey}"`, {
|
|
407
|
+
operation: "operations.aggregate",
|
|
408
|
+
suggestedAction: "Rebuild the client from the SDK build that ships this registry (the resource set is pinned by the plan)."
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const wireFilter = v.conditions.length === 0 ? void 0 : v.conditions;
|
|
412
|
+
if (v.groupBy.length === 0 && v.sum.length === 0) {
|
|
413
|
+
const raw = await holder.count({ filter: wireFilter });
|
|
414
|
+
const sentinel = typeof raw === "object" && raw !== null && "count" in raw ? raw : { count: raw };
|
|
415
|
+
const result2 = { mode: "count", count: sentinel.count, blind: sentinel.blindSurface !== void 0 };
|
|
416
|
+
if (sentinel.blindSurface !== void 0) result2.blindSurface = sentinel.blindSurface;
|
|
417
|
+
if (sentinel.emptyReason !== void 0) result2.emptyReason = sentinel.emptyReason;
|
|
418
|
+
return result2;
|
|
419
|
+
}
|
|
420
|
+
const res = await holder.queryBounded({ filter: wireFilter, limit: v.limit });
|
|
421
|
+
const sorted = foldGroups(res.items, v);
|
|
378
422
|
const top = sorted.slice(0, v.top);
|
|
379
423
|
const result = {
|
|
380
424
|
mode: v.groupBy.length > 0 ? "group" : "sum",
|
|
@@ -646,8 +690,7 @@ function parseMatchId(record) {
|
|
|
646
690
|
}
|
|
647
691
|
return { id: null, unsafe: true };
|
|
648
692
|
}
|
|
649
|
-
|
|
650
|
-
const operation = RECOVERY_CONFIRM;
|
|
693
|
+
function validateRecoveryInput(input, operation) {
|
|
651
694
|
if (!isRecord(input)) {
|
|
652
695
|
configError(
|
|
653
696
|
`recovery: input must be an object { resource, marker, ... } (got ${typeof input})`,
|
|
@@ -679,6 +722,9 @@ async function recovery(client, input, opts = {}) {
|
|
|
679
722
|
"Pass action: 'delete' for the destructive path (dry-run-first; the apply requires the confirm string)."
|
|
680
723
|
);
|
|
681
724
|
}
|
|
725
|
+
return { resource, marker, action };
|
|
726
|
+
}
|
|
727
|
+
function resolveRecoveryTarget(client, resource, operation) {
|
|
682
728
|
const entry = RELATIONSHIP_MAP[resource];
|
|
683
729
|
if (entry === void 0) {
|
|
684
730
|
throw new AutotaskError(
|
|
@@ -699,6 +745,9 @@ async function recovery(client, input, opts = {}) {
|
|
|
699
745
|
"Use a resource with a query surface, or address the record by id directly (get/fetchMany)."
|
|
700
746
|
);
|
|
701
747
|
}
|
|
748
|
+
return { entry, holder };
|
|
749
|
+
}
|
|
750
|
+
function resolveMarkerField(input, resource, operation) {
|
|
702
751
|
const field = input.field !== void 0 ? String(input.field) : MARKER_FIELDS[resource];
|
|
703
752
|
if (typeof field !== "string" || field.trim() === "") {
|
|
704
753
|
configError(
|
|
@@ -707,55 +756,126 @@ async function recovery(client, input, opts = {}) {
|
|
|
707
756
|
"Pass input.field \u2014 a name-like vendor field the marker was stamped into (see the resource model in autotask_describe / autotask_catalog)."
|
|
708
757
|
);
|
|
709
758
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
)
|
|
726
|
-
|
|
759
|
+
return field;
|
|
760
|
+
}
|
|
761
|
+
function resolveParentId(input, entry, resource, operation) {
|
|
762
|
+
if (input.parentId === void 0) return void 0;
|
|
763
|
+
if (!Number.isInteger(input.parentId) || input.parentId < 0) {
|
|
764
|
+
configError(
|
|
765
|
+
`recovery: input.parentId must be a non-negative integer (got ${String(input.parentId)})`,
|
|
766
|
+
operation,
|
|
767
|
+
"Pass the parent record id (e.g. the ticket id for a ticket note)."
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
if (entry.parent === null || entry.parent.field === null) {
|
|
771
|
+
configError(
|
|
772
|
+
`recovery: ${resource} is not a parent-scoped child resource \u2014 input.parentId does not apply`,
|
|
773
|
+
operation,
|
|
774
|
+
`Drop input.parentId (the search runs unscoped), or pass the child resource that IS scoped under ${resource}.`
|
|
775
|
+
);
|
|
727
776
|
}
|
|
777
|
+
return input.parentId;
|
|
778
|
+
}
|
|
779
|
+
function resolveExtraFilter(input, operation) {
|
|
728
780
|
const extra = [];
|
|
729
|
-
if (input.filter
|
|
730
|
-
|
|
781
|
+
if (input.filter === void 0) return extra;
|
|
782
|
+
if (!Array.isArray(input.filter)) {
|
|
783
|
+
configError(
|
|
784
|
+
`recovery: input.filter must be an array of leaf filter conditions (got ${typeof input.filter})`,
|
|
785
|
+
operation,
|
|
786
|
+
"Pass e.g. [{ field: 'status', op: 'eq', value: 'open' }] \u2014 leaf conditions only."
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
for (const c of input.filter) {
|
|
790
|
+
if (!isRecord(c) || typeof c.op !== "string" || c.op === "and" || c.op === "or") {
|
|
731
791
|
configError(
|
|
732
|
-
|
|
792
|
+
"recovery: input.filter accepts only plain leaf conditions ({ field, op, value }) \u2014 no grouping",
|
|
733
793
|
operation,
|
|
734
|
-
"Pass
|
|
794
|
+
"Pass flat leaf conditions; grouping operators are not supported here."
|
|
735
795
|
);
|
|
736
796
|
}
|
|
737
|
-
|
|
738
|
-
if (!isRecord(c) || typeof c.op !== "string" || c.op === "and" || c.op === "or") {
|
|
739
|
-
configError(
|
|
740
|
-
"recovery: input.filter accepts only plain leaf conditions ({ field, op, value }) \u2014 no grouping",
|
|
741
|
-
operation,
|
|
742
|
-
"Pass flat leaf conditions; grouping operators are not supported here."
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
extra.push(c);
|
|
746
|
-
}
|
|
797
|
+
extra.push(c);
|
|
747
798
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
799
|
+
return extra;
|
|
800
|
+
}
|
|
801
|
+
function resolveLimit(input, operation) {
|
|
802
|
+
if (input.limit === void 0) return RECOVERY_DEFAULT_LIMIT;
|
|
803
|
+
if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > RECOVERY_MAX_LIMIT) {
|
|
804
|
+
configError(
|
|
805
|
+
`recovery: input.limit must be an integer 1..${RECOVERY_MAX_LIMIT} (got ${String(input.limit)})`,
|
|
806
|
+
operation,
|
|
807
|
+
`Raise the marker precision instead of the cap, or pass limit up to ${RECOVERY_MAX_LIMIT}.`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
return input.limit;
|
|
811
|
+
}
|
|
812
|
+
async function applyRecoveryDeletes(holder, matches, parentId, resource, operation) {
|
|
813
|
+
const deleteMethod = holder.delete;
|
|
814
|
+
if (deleteMethod === void 0) {
|
|
815
|
+
throw new DeleteBoundaryError(
|
|
816
|
+
`recovery: ${resource} has no delete method on the client \u2014 the boundary is marked available in the plan but the typed delete surface is missing`,
|
|
817
|
+
{
|
|
753
818
|
operation,
|
|
754
|
-
|
|
755
|
-
|
|
819
|
+
suggestedAction: "Report the registry drift (plan says deletable, client exposes no delete) \u2014 or clean up via the Autotask UI.",
|
|
820
|
+
details: { resource }
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
const deleted = [];
|
|
825
|
+
for (const match of matches) {
|
|
826
|
+
const id = match.id;
|
|
827
|
+
let verify = "error";
|
|
828
|
+
let code;
|
|
829
|
+
try {
|
|
830
|
+
if (parentId !== void 0) {
|
|
831
|
+
await deleteMethod.call(holder, parentId, id, { dryRun: false });
|
|
832
|
+
} else {
|
|
833
|
+
await deleteMethod.call(holder, id, { dryRun: false });
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
if (parentId !== void 0) {
|
|
837
|
+
await holder.get.call(holder, parentId, id);
|
|
838
|
+
verify = "still-present";
|
|
839
|
+
} else {
|
|
840
|
+
await holder.get.call(holder, id);
|
|
841
|
+
verify = "still-present";
|
|
842
|
+
}
|
|
843
|
+
} catch (err) {
|
|
844
|
+
if (err instanceof NotFoundError) verify = "not-found";
|
|
845
|
+
else {
|
|
846
|
+
verify = "error";
|
|
847
|
+
code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch (err) {
|
|
851
|
+
code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
|
|
756
852
|
}
|
|
757
|
-
|
|
853
|
+
deleted.push({ id, verified: verify === "not-found", code });
|
|
758
854
|
}
|
|
855
|
+
return deleted;
|
|
856
|
+
}
|
|
857
|
+
function collectMatches(items, field) {
|
|
858
|
+
const matches = [];
|
|
859
|
+
const unsafeIds = [];
|
|
860
|
+
items.forEach((record, idx) => {
|
|
861
|
+
const parsed = parseMatchId(record);
|
|
862
|
+
if (parsed.unsafe) unsafeIds.push(idx);
|
|
863
|
+
const value = record[field];
|
|
864
|
+
matches.push({
|
|
865
|
+
id: parsed.id,
|
|
866
|
+
value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : null
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
return { matches, unsafeIds };
|
|
870
|
+
}
|
|
871
|
+
async function recovery(client, input, opts = {}) {
|
|
872
|
+
const operation = RECOVERY_CONFIRM;
|
|
873
|
+
const { resource, marker, action } = validateRecoveryInput(input, operation);
|
|
874
|
+
const { entry, holder } = resolveRecoveryTarget(client, resource, operation);
|
|
875
|
+
const field = resolveMarkerField(input, resource, operation);
|
|
876
|
+
const parentId = resolveParentId(input, entry, resource, operation);
|
|
877
|
+
const extra = resolveExtraFilter(input, operation);
|
|
878
|
+
const limit = resolveLimit(input, operation);
|
|
759
879
|
const boundaryNote = DELETE_BOUNDARY_NOTES[resource];
|
|
760
880
|
const boundary = {
|
|
761
881
|
available: boundaryNote === void 0,
|
|
@@ -805,17 +925,7 @@ async function recovery(client, input, opts = {}) {
|
|
|
805
925
|
}
|
|
806
926
|
);
|
|
807
927
|
}
|
|
808
|
-
const matches =
|
|
809
|
-
const unsafeIds = [];
|
|
810
|
-
query.items.forEach((record, idx) => {
|
|
811
|
-
const parsed = parseMatchId(record);
|
|
812
|
-
if (parsed.unsafe) unsafeIds.push(idx);
|
|
813
|
-
const value = record[field];
|
|
814
|
-
matches.push({
|
|
815
|
-
id: parsed.id,
|
|
816
|
-
value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : null
|
|
817
|
-
});
|
|
818
|
-
});
|
|
928
|
+
const { matches, unsafeIds } = collectMatches(query.items, field);
|
|
819
929
|
const unsafeId = unsafeIds.length > 0;
|
|
820
930
|
const base = {
|
|
821
931
|
resource,
|
|
@@ -844,54 +954,11 @@ async function recovery(client, input, opts = {}) {
|
|
|
844
954
|
}
|
|
845
955
|
);
|
|
846
956
|
}
|
|
847
|
-
const
|
|
848
|
-
if (deleteMethod === void 0) {
|
|
849
|
-
throw new DeleteBoundaryError(
|
|
850
|
-
`recovery: ${resource} has no delete method on the client \u2014 the boundary is marked available in the plan but the typed delete surface is missing`,
|
|
851
|
-
{
|
|
852
|
-
operation,
|
|
853
|
-
suggestedAction: "Report the registry drift (plan says deletable, client exposes no delete) \u2014 or clean up via the Autotask UI.",
|
|
854
|
-
details: { resource }
|
|
855
|
-
}
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
const deleted = [];
|
|
859
|
-
for (const match of matches) {
|
|
860
|
-
const id = match.id;
|
|
861
|
-
let verify = "error";
|
|
862
|
-
let code;
|
|
863
|
-
try {
|
|
864
|
-
if (parentId !== void 0) {
|
|
865
|
-
await deleteMethod.call(holder, parentId, id, { dryRun: false });
|
|
866
|
-
} else {
|
|
867
|
-
await deleteMethod.call(holder, id, { dryRun: false });
|
|
868
|
-
}
|
|
869
|
-
try {
|
|
870
|
-
if (parentId !== void 0) {
|
|
871
|
-
await holder.get.call(holder, parentId, id);
|
|
872
|
-
verify = "still-present";
|
|
873
|
-
} else {
|
|
874
|
-
await holder.get.call(holder, id);
|
|
875
|
-
verify = "still-present";
|
|
876
|
-
}
|
|
877
|
-
} catch (err) {
|
|
878
|
-
if (err instanceof NotFoundError) verify = "not-found";
|
|
879
|
-
else {
|
|
880
|
-
verify = "error";
|
|
881
|
-
code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
} catch (err) {
|
|
885
|
-
code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
|
|
886
|
-
}
|
|
887
|
-
deleted.push({ id, verified: verify === "not-found", code });
|
|
888
|
-
}
|
|
957
|
+
const deleted = await applyRecoveryDeletes(holder, matches, parentId, resource, operation);
|
|
889
958
|
return { ...base, deleted, dryRun: false };
|
|
890
959
|
}
|
|
891
960
|
|
|
892
|
-
// src/operations/invoke.ts
|
|
893
|
-
var RECORDS = CAPABILITIES;
|
|
894
|
-
var byId = new Map(RECORDS.map((r) => [r.id, r]));
|
|
961
|
+
// src/operations/invoke/modes.ts
|
|
895
962
|
function needsConfirmation(record) {
|
|
896
963
|
return record.effect === "destructive" || (record.flags ?? []).includes("requiresApproval");
|
|
897
964
|
}
|
|
@@ -924,6 +991,8 @@ function crossModeRefusal(operation, effect, mode) {
|
|
|
924
991
|
const target = actual === "read" ? `Route it through the read mode (${MODE_SDK[actual]} / ${MODE_TOOL[actual]}).` : actual === "write" ? `Route it through the write mode (${MODE_SDK[actual]} / ${MODE_TOOL[actual]} \u2014 dry-run first).` : `Deletes have their own tool (${MODE_TOOL[actual]}) and require confirm: "${operation}".`;
|
|
925
992
|
return `invoke: "${operation}" is ${subject} (effect=${effect}) \u2014 refused on the ${mode} path (${MODE_TOOL[mode]} routes effect=${MODE_EFFECT[mode]} operations only). ${target}`;
|
|
926
993
|
}
|
|
994
|
+
|
|
995
|
+
// src/operations/invoke/validate.ts
|
|
927
996
|
function isSafeNonNegativeInteger(value) {
|
|
928
997
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
929
998
|
}
|
|
@@ -957,8 +1026,11 @@ function validateDispatchArgs(record, argsIn) {
|
|
|
957
1026
|
const schema = record.inputSchema ?? {};
|
|
958
1027
|
const names = Object.keys(schema);
|
|
959
1028
|
if (args.length > names.length) {
|
|
1029
|
+
const bagName = names.find((n) => schema[n].properties);
|
|
1030
|
+
const surplus = args[names.length];
|
|
1031
|
+
const bagHint = bagName && surplus !== null && typeof surplus === "object" && !Array.isArray(surplus) ? ` \u2014 options go inside the single ${bagName} object` : "";
|
|
960
1032
|
throw new AutotaskConfigError(
|
|
961
|
-
`invoke: "${operation}" declares ${names.length} positional arg(s) (${declaredArgList(record)}) but ${args.length} were passed \u2014 extra args are refused
|
|
1033
|
+
`invoke: "${operation}" declares ${names.length} positional arg(s) (${declaredArgList(record)}) but ${args.length} were passed \u2014 extra args are refused${bagHint}`,
|
|
962
1034
|
{
|
|
963
1035
|
operation,
|
|
964
1036
|
suggestedAction: `Pass exactly the declared args in order (${declaredArgList(record)}); dryRun/confirm/lastModifiedDate travel through the invoke options, never the args.`
|
|
@@ -985,6 +1057,8 @@ function validateDispatchArgs(record, argsIn) {
|
|
|
985
1057
|
}
|
|
986
1058
|
}
|
|
987
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
// src/operations/invoke/inputSets.ts
|
|
988
1062
|
var PARAMS_BAG_KEYS = {
|
|
989
1063
|
query: ["filter", "limit", "includeFields", "via", "latest", "noteChars", "fieldsPreset", "fieldCheck"],
|
|
990
1064
|
// No registry row DECLARES `queryAll` today (a plan decision: the raw walk is
|
|
@@ -1050,116 +1124,6 @@ function validateParamsBag(record, args) {
|
|
|
1050
1124
|
}
|
|
1051
1125
|
);
|
|
1052
1126
|
}
|
|
1053
|
-
async function liveFieldNames(holder) {
|
|
1054
|
-
const reader = holder.getFieldInfo;
|
|
1055
|
-
if (typeof reader !== "function") return null;
|
|
1056
|
-
try {
|
|
1057
|
-
const rows = await reader.call(holder);
|
|
1058
|
-
if (!Array.isArray(rows)) return null;
|
|
1059
|
-
const names = [];
|
|
1060
|
-
for (const row of rows) {
|
|
1061
|
-
if (typeof row.name === "string") names.push(row.name);
|
|
1062
|
-
}
|
|
1063
|
-
return names.length > 0 ? names : null;
|
|
1064
|
-
} catch (err) {
|
|
1065
|
-
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
1066
|
-
return null;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
var UNKNOWN_NAME_LIST_CAP = 20;
|
|
1070
|
-
var UNKNOWN_NAME_DISPLAY_CHARS = 80;
|
|
1071
|
-
function formatUnknownNameList(names) {
|
|
1072
|
-
const listed = names.slice(0, UNKNOWN_NAME_LIST_CAP).map((n) => `"${n.length > UNKNOWN_NAME_DISPLAY_CHARS ? `${n.slice(0, UNKNOWN_NAME_DISPLAY_CHARS)}\u2026` : n}"`).join(", ");
|
|
1073
|
-
const more = names.length > UNKNOWN_NAME_LIST_CAP ? ` (and ${names.length - UNKNOWN_NAME_LIST_CAP} more)` : "";
|
|
1074
|
-
return `${listed}${more}`;
|
|
1075
|
-
}
|
|
1076
|
-
function collectFilterFieldNames(conditions) {
|
|
1077
|
-
const out = [];
|
|
1078
|
-
const walk = (conds) => {
|
|
1079
|
-
for (const cond of conds) {
|
|
1080
|
-
if (!isRecord(cond)) continue;
|
|
1081
|
-
if (cond.op === "and" || cond.op === "or") {
|
|
1082
|
-
if (Array.isArray(cond.items)) walk(cond.items);
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
1085
|
-
if (cond.udf === true) continue;
|
|
1086
|
-
if (typeof cond.field === "string" && cond.field.length > 0) out.push(cond.field);
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
walk(conditions);
|
|
1090
|
-
return out;
|
|
1091
|
-
}
|
|
1092
|
-
async function validateQueryFieldNames(record, holder, args) {
|
|
1093
|
-
const bag = args[0];
|
|
1094
|
-
if (!isRecord(bag)) return;
|
|
1095
|
-
const fieldCheck = bag["fieldCheck"];
|
|
1096
|
-
if (fieldCheck !== void 0 && typeof fieldCheck !== "boolean") {
|
|
1097
|
-
throw new AutotaskConfigError(
|
|
1098
|
-
`invoke: "${record.id}" params bag fieldCheck must be a boolean (got ${typeof fieldCheck}) \u2014 the request was not issued`,
|
|
1099
|
-
{ operation: record.id, suggestedAction: "Pass fieldCheck: true (default) or fieldCheck: false (disable the field-name checks \u2014 includeFields AND filter condition names)." }
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
const includeFields = bag["includeFields"];
|
|
1103
|
-
let includeNames;
|
|
1104
|
-
if (includeFields !== void 0) {
|
|
1105
|
-
let shapeOk = Array.isArray(includeFields);
|
|
1106
|
-
const names = [];
|
|
1107
|
-
if (shapeOk) {
|
|
1108
|
-
for (const entry of includeFields) {
|
|
1109
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
1110
|
-
shapeOk = false;
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1113
|
-
names.push(entry);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
if (!shapeOk) {
|
|
1117
|
-
throw new AutotaskConfigError(
|
|
1118
|
-
`invoke: "${record.id}" params bag includeFields must be an array of non-empty field-name strings \u2014 the request was not issued`,
|
|
1119
|
-
{
|
|
1120
|
-
operation: record.id,
|
|
1121
|
-
suggestedAction: 'Pass includeFields as an array of exact vendor field names, e.g. ["id", "title"]; the live set is in operations.fieldCatalog.'
|
|
1122
|
-
}
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
includeNames = names;
|
|
1126
|
-
}
|
|
1127
|
-
let filterNames;
|
|
1128
|
-
if (bag["filter"] !== void 0) {
|
|
1129
|
-
const conditions = normalizedConditions(bag["filter"]);
|
|
1130
|
-
filterNames = collectFilterFieldNames(conditions ?? []);
|
|
1131
|
-
}
|
|
1132
|
-
if (fieldCheck === false) return;
|
|
1133
|
-
const hasInclude = includeNames !== void 0 && includeNames.length > 0;
|
|
1134
|
-
const hasFilter = filterNames !== void 0 && filterNames.length > 0;
|
|
1135
|
-
if (!hasInclude && !hasFilter) return;
|
|
1136
|
-
const live = await liveFieldNames(holder);
|
|
1137
|
-
let known;
|
|
1138
|
-
let liveOnlyExempt = [];
|
|
1139
|
-
if (live !== null) {
|
|
1140
|
-
known = live;
|
|
1141
|
-
} else {
|
|
1142
|
-
const vendored = MODEL_FIELD_SETS[record.resource];
|
|
1143
|
-
liveOnlyExempt = Array.isArray(record.metadata?.liveOnlyFields) ? record.metadata.liveOnlyFields : [];
|
|
1144
|
-
known = vendored === void 0 ? void 0 : [...vendored, ...liveOnlyExempt];
|
|
1145
|
-
}
|
|
1146
|
-
if (known === void 0) return;
|
|
1147
|
-
const authority = live !== null ? "the tenant's live entity field metadata" : liveOnlyExempt.length > 0 ? "the vendored spec (src/field-sets.ts) + the plan-pinned live-only fields (exempt from the vendored refusal)" : "the vendored spec (src/field-sets.ts)";
|
|
1148
|
-
const unknownInclude = hasInclude ? includeNames.filter((name) => !known.includes(name)) : [];
|
|
1149
|
-
const unknownFilter = hasFilter ? [...new Set(filterNames.filter((name) => !known.includes(name)))] : [];
|
|
1150
|
-
if (unknownInclude.length === 0 && unknownFilter.length === 0) return;
|
|
1151
|
-
const parts = [];
|
|
1152
|
-
if (unknownInclude.length > 0) parts.push(`includeFields has unknown field name(s) ${formatUnknownNameList(unknownInclude)}`);
|
|
1153
|
-
if (unknownFilter.length > 0) parts.push(`filter condition(s) carry unknown field name(s) ${formatUnknownNameList(unknownFilter)}`);
|
|
1154
|
-
const wireClause = record.operation === "count" ? "the count request was not issued" : "the query POST was not issued";
|
|
1155
|
-
throw new AutotaskConfigError(
|
|
1156
|
-
`invoke: "${record.id}" ${parts.join("; ")} against ${authority} \u2014 ${wireClause}`,
|
|
1157
|
-
{
|
|
1158
|
-
operation: record.id,
|
|
1159
|
-
suggestedAction: `Use exact vendor field names (operations.fieldCatalog lists the live set for the entity); pass fieldCheck: false to disable this name check. The authority here was ${authority}.`
|
|
1160
|
-
}
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
1127
|
var INVOKE_OPTION_KEYS = ["dryRun", "confirm", "lastModifiedDate", "maxRetries", "mode"];
|
|
1164
1128
|
function validateInvokeOptions(opts, operation) {
|
|
1165
1129
|
if (opts === void 0 || opts === null) return;
|
|
@@ -1187,8 +1151,25 @@ function unknownKeysRefusal(raw, validKeys, subject, operation) {
|
|
|
1187
1151
|
}
|
|
1188
1152
|
);
|
|
1189
1153
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1154
|
+
|
|
1155
|
+
// src/operations/invoke/registry.ts
|
|
1156
|
+
var RECORDS = CAPABILITIES;
|
|
1157
|
+
var byId = new Map(RECORDS.map((r) => [r.id, r]));
|
|
1158
|
+
|
|
1159
|
+
// src/operations/invoke/dispatch.ts
|
|
1160
|
+
var INVOKE_MAX_INLINE_BYTES = 1024 * 1024;
|
|
1161
|
+
function bufferResult(buf) {
|
|
1162
|
+
if (buf.byteLength <= INVOKE_MAX_INLINE_BYTES) {
|
|
1163
|
+
return { encoding: "base64", byteLength: buf.byteLength, truncated: false, data: buf.toString("base64") };
|
|
1164
|
+
}
|
|
1165
|
+
return {
|
|
1166
|
+
encoding: "base64",
|
|
1167
|
+
byteLength: buf.byteLength,
|
|
1168
|
+
truncated: true,
|
|
1169
|
+
note: `Body exceeds the ${INVOKE_MAX_INLINE_BYTES}-byte inline limit and was not returned; call the resource's download method directly.`
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function resolveRecord(operation) {
|
|
1192
1173
|
const record = byId.get(operation);
|
|
1193
1174
|
if (record === void 0) {
|
|
1194
1175
|
throw new AutotaskConfigError(
|
|
@@ -1196,6 +1177,9 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1196
1177
|
{ operation, suggestedAction: "List the registry (the ./capabilities subpath) for the exact operation key." }
|
|
1197
1178
|
);
|
|
1198
1179
|
}
|
|
1180
|
+
return record;
|
|
1181
|
+
}
|
|
1182
|
+
function governPreRequest(record, operation, opts) {
|
|
1199
1183
|
validateInvokeOptions(opts, operation);
|
|
1200
1184
|
const mode = opts.mode;
|
|
1201
1185
|
if (mode !== void 0 && MODE_BY_EFFECT[record.effect] !== mode) {
|
|
@@ -1207,8 +1191,7 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1207
1191
|
{ operation, suggestedAction: "Pass dryRun: true (simulate) or dryRun: false (apply) as a real boolean." }
|
|
1208
1192
|
);
|
|
1209
1193
|
}
|
|
1210
|
-
|
|
1211
|
-
if (!isWrite && opts.dryRun === true) {
|
|
1194
|
+
if (record.effect === "read" && opts.dryRun === true) {
|
|
1212
1195
|
throw new AutotaskConfigError(
|
|
1213
1196
|
`invoke: "${operation}" is a read \u2014 dryRun is not applicable (reads issue their request as-is)`,
|
|
1214
1197
|
{ operation }
|
|
@@ -1221,53 +1204,57 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1221
1204
|
"Pass confirm equal to the operation key to acknowledge the deliberate act."
|
|
1222
1205
|
);
|
|
1223
1206
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1207
|
+
}
|
|
1208
|
+
function operationsHolder(client) {
|
|
1209
|
+
return {
|
|
1210
|
+
resolveAny: (identifier, opts) => resolveAny(client, identifier, opts),
|
|
1211
|
+
searchAcrossResources: (query, opts) => searchAcrossResources(client, query, opts),
|
|
1212
|
+
fetchMany: (items, opts) => fetchMany(client, items, opts),
|
|
1213
|
+
// Issue #62: the generic aggregate + the cheap count — the
|
|
1214
|
+
// groupBy/sum-absent mode is ONE vendor /query/count (the S12-C blind
|
|
1215
|
+
// sentinel rides through); the group/sum modes walk the named
|
|
1216
|
+
// <resource>.query op via the house bounded walk and fold client-side
|
|
1217
|
+
// (truncation honesty: hasMore/totalScanned — no silent partial sums).
|
|
1218
|
+
aggregate: (operation, filter, opts) => aggregate(client, operation, filter, opts),
|
|
1219
|
+
// Issue #59 PR-B: the bulk fetch by ids — query-first chunked `id in`
|
|
1220
|
+
// reads + the per-id `get` fallback (blind-surface-safe; per-item
|
|
1221
|
+
// typed outcomes reuse the fetchMany shapes).
|
|
1222
|
+
fetchByIds: (items, opts) => fetchByIds(client, items, opts),
|
|
1223
|
+
// Issue #65: opt-in resource name expansion — per-id resources.get
|
|
1224
|
+
// reads ONLY (never an id-in query — the resources table can be blind
|
|
1225
|
+
// to count/query while per-id reads work); blind-marked rows for ids
|
|
1226
|
+
// the API user cannot resolve; the name cache (default-on).
|
|
1227
|
+
expandResourceNames: (ids, opts) => expandResourceNames(client, ids, opts),
|
|
1228
|
+
relationshipMap: (resourceKey) => relationshipMap(client, resourceKey),
|
|
1229
|
+
// Issue #11: the served blind-surface inventory (zero wire) + the
|
|
1230
|
+
// bounded caller-opted reachability sweep (read-only, confirmProbe-
|
|
1231
|
+
// required — the op's own pre-request gates keep it deliberate).
|
|
1232
|
+
blindSurfaces: (opts) => blindSurfaces(client, opts),
|
|
1233
|
+
probeReachability: (opts) => probeReachability(client, opts),
|
|
1234
|
+
// Issue #12: the served zone-fact create-guidance catalog (zero wire).
|
|
1235
|
+
createGuidance: (opts) => createGuidance(client, opts),
|
|
1236
|
+
// Issue #202: ExecuteCommand deep links — pure, zero wire.
|
|
1237
|
+
uiLink: (input) => uiLink(client, input),
|
|
1238
|
+
// Issue #31: the opt-in live field-metadata channel — N bounded
|
|
1239
|
+
// entityInformation GETs (the op's own cap refuses over-cap fan-outs
|
|
1240
|
+
// pre-request; per-resource failures are recorded on the row and
|
|
1241
|
+
// never abort the call).
|
|
1242
|
+
fieldCatalog: (opts) => fieldCatalog(client, opts),
|
|
1243
|
+
// Issue #9: the one-call recovery/cleanup op — effect write in the
|
|
1244
|
+
// registry (the governor's dry-run-first applies to it); the op's own
|
|
1245
|
+
// confirm gate makes the destructive action deliberate (the governor
|
|
1246
|
+
// forwards the caller's confirm through the trailing bag below).
|
|
1247
|
+
recovery: (input, opts) => recovery(client, input, opts)
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function resolveHolder(client, record, operation) {
|
|
1251
|
+
const clientBag = client;
|
|
1252
|
+
let holder = clientBag[camelKey(record.resource)];
|
|
1229
1253
|
if (typeof holder === "function") {
|
|
1230
|
-
holder =
|
|
1254
|
+
holder = clientBag[`${camelKey(record.resource)}Resource`] ?? holder;
|
|
1231
1255
|
}
|
|
1232
1256
|
if (holder === void 0 && record.resource === "operations") {
|
|
1233
|
-
holder =
|
|
1234
|
-
resolveAny: (identifier, opts2) => resolveAny(client, identifier, opts2),
|
|
1235
|
-
searchAcrossResources: (query, opts2) => searchAcrossResources(client, query, opts2),
|
|
1236
|
-
fetchMany: (items, opts2) => fetchMany(client, items, opts2),
|
|
1237
|
-
// Issue #62: the generic aggregate + the cheap count — the
|
|
1238
|
-
// groupBy/sum-absent mode is ONE vendor /query/count (the S12-C blind
|
|
1239
|
-
// sentinel rides through); the group/sum modes walk the named
|
|
1240
|
-
// <resource>.query op via the house bounded walk and fold client-side
|
|
1241
|
-
// (truncation honesty: hasMore/totalScanned — no silent partial sums).
|
|
1242
|
-
aggregate: (operation2, filter, opts2) => aggregate(client, operation2, filter, opts2),
|
|
1243
|
-
// Issue #59 PR-B: the bulk fetch by ids — query-first chunked `id in`
|
|
1244
|
-
// reads + the per-id `get` fallback (blind-surface-safe; per-item
|
|
1245
|
-
// typed outcomes reuse the fetchMany shapes).
|
|
1246
|
-
fetchByIds: (items, opts2) => fetchByIds(client, items, opts2),
|
|
1247
|
-
// Issue #65: opt-in resource name expansion — per-id resources.get
|
|
1248
|
-
// reads ONLY (never an id-in query — the resources table can be blind
|
|
1249
|
-
// to count/query while per-id reads work); blind-marked rows for ids
|
|
1250
|
-
// the API user cannot resolve; the name cache (default-on).
|
|
1251
|
-
expandResourceNames: (ids, opts2) => expandResourceNames(client, ids, opts2),
|
|
1252
|
-
relationshipMap: (resourceKey) => relationshipMap(client, resourceKey),
|
|
1253
|
-
// Issue #11: the served blind-surface inventory (zero wire) + the
|
|
1254
|
-
// bounded caller-opted reachability sweep (read-only, confirmProbe-
|
|
1255
|
-
// required — the op's own pre-request gates keep it deliberate).
|
|
1256
|
-
blindSurfaces: (opts2) => blindSurfaces(client, opts2),
|
|
1257
|
-
probeReachability: (opts2) => probeReachability(client, opts2),
|
|
1258
|
-
// Issue #12: the served zone-fact create-guidance catalog (zero wire).
|
|
1259
|
-
createGuidance: (opts2) => createGuidance(client, opts2),
|
|
1260
|
-
// Issue #31: the opt-in live field-metadata channel — N bounded
|
|
1261
|
-
// entityInformation GETs (the op's own cap refuses over-cap fan-outs
|
|
1262
|
-
// pre-request; per-resource failures are recorded on the row and
|
|
1263
|
-
// never abort the call).
|
|
1264
|
-
fieldCatalog: (opts2) => fieldCatalog(client, opts2),
|
|
1265
|
-
// Issue #9: the one-call recovery/cleanup op — effect write in the
|
|
1266
|
-
// registry (the governor's dry-run-first applies to it); the op's own
|
|
1267
|
-
// confirm gate makes the destructive action deliberate (the governor
|
|
1268
|
-
// forwards the caller's confirm through the trailing bag below).
|
|
1269
|
-
recovery: (input, opts2) => recovery(client, input, opts2)
|
|
1270
|
-
};
|
|
1257
|
+
holder = operationsHolder(client);
|
|
1271
1258
|
}
|
|
1272
1259
|
if (holder === void 0) {
|
|
1273
1260
|
throw new AutotaskConfigError(
|
|
@@ -1275,15 +1262,13 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1275
1262
|
{ operation }
|
|
1276
1263
|
);
|
|
1277
1264
|
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
);
|
|
1286
|
-
}
|
|
1265
|
+
return holder;
|
|
1266
|
+
}
|
|
1267
|
+
var BAG_RIDING_ARGS = {
|
|
1268
|
+
createIfNotExists: ["dedupFields"],
|
|
1269
|
+
moveConfigurationItem: ["destinationCompanyId"]
|
|
1270
|
+
};
|
|
1271
|
+
function buildMutationOpts(opts, isWrite) {
|
|
1287
1272
|
const dryRun = isWrite ? opts.dryRun !== false : false;
|
|
1288
1273
|
const mutationOpts = { dryRun };
|
|
1289
1274
|
if (isWrite && opts.lastModifiedDate !== void 0) {
|
|
@@ -1295,10 +1280,9 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1295
1280
|
if (isWrite && opts.confirm !== void 0) {
|
|
1296
1281
|
mutationOpts.confirm = opts.confirm;
|
|
1297
1282
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
};
|
|
1283
|
+
return mutationOpts;
|
|
1284
|
+
}
|
|
1285
|
+
function buildCallArgs(record, args, mutationOpts, isWrite) {
|
|
1302
1286
|
const declaredNames = Object.keys(record.inputSchema ?? {});
|
|
1303
1287
|
const bagRiding = new Set(BAG_RIDING_ARGS[record.operation] ?? []);
|
|
1304
1288
|
let positional = args;
|
|
@@ -1315,21 +1299,34 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
|
1315
1299
|
while (callArgs.length < declaredPositional.length) callArgs.push(void 0);
|
|
1316
1300
|
callArgs.push(mutationOpts);
|
|
1317
1301
|
}
|
|
1318
|
-
|
|
1319
|
-
return Buffer.isBuffer(result) ? bufferResult(result) : result;
|
|
1302
|
+
return callArgs;
|
|
1320
1303
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1304
|
+
async function invokeMode(client, operation, argsIn = [], opts = {}) {
|
|
1305
|
+
const args = argsIn ?? [];
|
|
1306
|
+
const record = resolveRecord(operation);
|
|
1307
|
+
governPreRequest(record, operation, opts);
|
|
1308
|
+
validateDispatchArgs(record, args);
|
|
1309
|
+
validateParamsBag(record, args);
|
|
1310
|
+
const boundedQuery = record.kind === "primitive" && record.operation === "query";
|
|
1311
|
+
const countedQuery = record.kind === "primitive" && record.operation === "count";
|
|
1312
|
+
const holder = resolveHolder(client, record, operation);
|
|
1313
|
+
if (boundedQuery || countedQuery) await validateQueryFieldNames(record, holder, args);
|
|
1314
|
+
const methodName = boundedQuery ? "queryBounded" : record.operation;
|
|
1315
|
+
const method = holder[methodName];
|
|
1316
|
+
if (typeof method !== "function") {
|
|
1317
|
+
throw new AutotaskConfigError(
|
|
1318
|
+
`invoke: "${record.resource}.${methodName}" is not a callable method on the client`,
|
|
1319
|
+
{ operation }
|
|
1320
|
+
);
|
|
1325
1321
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
};
|
|
1322
|
+
const isWrite = record.effect !== "read";
|
|
1323
|
+
const mutationOpts = buildMutationOpts(opts, isWrite);
|
|
1324
|
+
const callArgs = buildCallArgs(record, args, mutationOpts, isWrite);
|
|
1325
|
+
const result = await method.apply(holder, callArgs);
|
|
1326
|
+
return Buffer.isBuffer(result) ? bufferResult(result) : result;
|
|
1332
1327
|
}
|
|
1328
|
+
|
|
1329
|
+
// src/operations/invoke/wrappers.ts
|
|
1333
1330
|
async function invoke(client, operation, args = [], opts = {}) {
|
|
1334
1331
|
return invokeMode(client, operation, args, { ...opts, mode: void 0 });
|
|
1335
1332
|
}
|
|
@@ -1362,9 +1359,11 @@ export {
|
|
|
1362
1359
|
RECOVERY_CONFIRM,
|
|
1363
1360
|
RECOVERY_DEFAULT_LIMIT,
|
|
1364
1361
|
RECOVERY_MAX_LIMIT,
|
|
1362
|
+
UI_LINK_ENTITIES,
|
|
1365
1363
|
WORKFLOW_RESOURCES,
|
|
1366
1364
|
aggregate,
|
|
1367
1365
|
blindSurfaces,
|
|
1366
|
+
buildUiLink,
|
|
1368
1367
|
createGuidance,
|
|
1369
1368
|
dateBucketLabel,
|
|
1370
1369
|
expandResourceNames,
|
|
@@ -1381,6 +1380,7 @@ export {
|
|
|
1381
1380
|
relationshipMap,
|
|
1382
1381
|
resolveAny,
|
|
1383
1382
|
searchAcrossResources,
|
|
1383
|
+
uiLink,
|
|
1384
1384
|
unknownKeysRefusal,
|
|
1385
1385
|
validateDispatchArgs,
|
|
1386
1386
|
validateFetchManyItems,
|