node-autotask 0.40.0 → 0.42.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 +32 -0
- package/MCP_TOOL_CATALOG.json +147 -149
- package/MCP_TOOL_MANIFEST.md +9 -9
- package/N8N_MCP_MANIFEST.json +4 -43
- package/N8N_MCP_MANIFEST.md +6 -7
- package/README.md +3 -4
- package/capabilities.json +3945 -3405
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-VDVBXXJW.js → chunk-AETFO6BO.js} +1765 -1880
- package/dist/{chunk-EUGSF3U4.cjs → chunk-HD5BRA3Z.cjs} +285 -285
- package/dist/{chunk-E5W24BZB.cjs → chunk-MYL34QU5.cjs} +143 -143
- package/dist/{chunk-FEYVLIHD.cjs → chunk-SEFJJY4J.cjs} +2801 -2916
- package/dist/{chunk-O32L6GS5.js → chunk-TTPHVFPJ.js} +2 -2
- package/dist/{chunk-EVDVESQM.cjs → chunk-VEOSOIU4.cjs} +3944 -3404
- package/dist/{chunk-Y6YJYKBZ.js → chunk-WP33PQVM.js} +3944 -3404
- package/dist/{chunk-MTFRSD7P.js → chunk-ZR24JGW2.js} +2 -2
- package/dist/index-C3KFMnRT.d.cts +6860 -0
- package/dist/index-C3KFMnRT.d.ts +6860 -0
- package/dist/{index-BKp8_P2L.d.cts → index-C7rMJ4IV.d.ts} +139 -7236
- package/dist/{index-BKp8_P2L.d.ts → index-Cm_Xpt7X.d.cts} +139 -7236
- package/dist/index.cjs +22 -24
- package/dist/index.d.cts +58 -34
- package/dist/index.d.ts +58 -34
- package/dist/index.js +8 -10
- package/dist/operations/index.cjs +363 -242
- package/dist/operations/index.d.cts +281 -25
- package/dist/operations/index.d.ts +281 -25
- package/dist/operations/index.js +178 -57
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +2 -1
- package/dist/pool.d.ts +2 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +2 -1
- package/dist/resources/index.d.ts +2 -1
- 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 +101 -139
- package/docs/LIVE-TESTING.md +9 -5
- package/docs/N8N-ADAPTER.md +1 -1
- package/docs/RELEASING.md +121 -0
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CAPABILITIES
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WP33PQVM.js";
|
|
4
4
|
import {
|
|
5
5
|
wrapUntrusted
|
|
6
6
|
} from "./chunk-5VHSLJMR.js";
|
|
@@ -486,7 +486,7 @@ function suggestedActionForStatus(status) {
|
|
|
486
486
|
case 429:
|
|
487
487
|
return "Back off and retry; honour Retry-After.";
|
|
488
488
|
default:
|
|
489
|
-
return status >= 500 ? "
|
|
489
|
+
return status >= 500 ? "The SDK already retried per its policy. A repeated 500 on one entity while others work usually means the API user lacks permission on that entity \u2014 ask the Autotask admin to grant it; otherwise retry once more at most." : void 0;
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
function errorFromStatus(status, body, url, operation, redactPersonalData = true, credential) {
|
|
@@ -586,6 +586,40 @@ var MoveRefusedError = class extends AutotaskError {
|
|
|
586
586
|
}
|
|
587
587
|
};
|
|
588
588
|
|
|
589
|
+
// src/errors/scopeRequired.ts
|
|
590
|
+
var UNSCOPED_WALK_THRESHOLD = 500;
|
|
591
|
+
var ScopeRequiredError = class extends AutotaskConfigError {
|
|
592
|
+
/** True when the call was refused locally: the walk was never issued. */
|
|
593
|
+
notSent = true;
|
|
594
|
+
/** The refusal is a caller-input problem (fix the filter; do not retry). */
|
|
595
|
+
callerInput = true;
|
|
596
|
+
/**
|
|
597
|
+
* @param operation The operation the refusal is about (e.g.
|
|
598
|
+
* `billing_items.query` — the op the caller's filter must scope).
|
|
599
|
+
* @param details `{ threshold }` + the pre-flight `count` when the
|
|
600
|
+
* guard spent one (the aggregate's pre-request refusal has no count).
|
|
601
|
+
* @param reason The per-surface refusal clause (default
|
|
602
|
+
* `'no-id-scope'` — the query guard; the aggregate surface passes
|
|
603
|
+
* `'empty-filter'`, its fold refusing on an EMPTY filter).
|
|
604
|
+
*/
|
|
605
|
+
constructor(operation, details, reason = "no-id-scope") {
|
|
606
|
+
const countClause = details.count !== void 0 ? `${details.count} rows match ` : "The unscoped walk ";
|
|
607
|
+
const reasonClause = reason === "empty-filter" ? `has no filter (over ${details.threshold}) \u2014 the fold would walk the whole tenant table \u2014 ` : `with no id scope in the filter (over ${details.threshold}) \u2014 `;
|
|
608
|
+
super(
|
|
609
|
+
`${countClause}${reasonClause}"${operation}" was not issued`,
|
|
610
|
+
{
|
|
611
|
+
code: "SCOPE_REQUIRED",
|
|
612
|
+
category: "validation",
|
|
613
|
+
operation,
|
|
614
|
+
retryable: false,
|
|
615
|
+
suggestedAction: "Filter on an id field with op eq or in (companyID, contractID, ticketID, resourceID, or the entity id \u2014 time entries: ticketID), or use a count / group / sum for totals, or pass acknowledgeUnscoped: true to walk it anyway.",
|
|
616
|
+
details: { threshold: details.threshold, ...details.count !== void 0 ? { count: details.count } : {} }
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
this.name = "ScopeRequiredError";
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
589
623
|
// src/cache.ts
|
|
590
624
|
var READ_CACHE_MAX_ENTRIES = 4096;
|
|
591
625
|
var REFERENCE_TTL_MS = 36e5;
|
|
@@ -1300,6 +1334,15 @@ function resolveConfig(config) {
|
|
|
1300
1334
|
const caches = resolveCaches(config);
|
|
1301
1335
|
const update = { putFallback: config.update?.putFallback === true };
|
|
1302
1336
|
const reResolveZoneOn401 = config.reResolveZoneOn401 === true;
|
|
1337
|
+
let labels = false;
|
|
1338
|
+
if (config.labels !== void 0) {
|
|
1339
|
+
if (typeof config.labels !== "boolean") {
|
|
1340
|
+
throw new AutotaskConfigError(
|
|
1341
|
+
"labels must be a boolean (true adds the *Name/*Label row siblings to the read surfaces, false returns raw ids)"
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
labels = config.labels;
|
|
1345
|
+
}
|
|
1303
1346
|
const logger = config.logger ?? NoopLogger;
|
|
1304
1347
|
if (credentials.baseUrl.startsWith("http:")) {
|
|
1305
1348
|
const host = new URL(credentials.baseUrl).host;
|
|
@@ -1315,6 +1358,7 @@ function resolveConfig(config) {
|
|
|
1315
1358
|
userName: credentials.userName,
|
|
1316
1359
|
secret: credentials.secret,
|
|
1317
1360
|
impersonationResourceId: credentials.impersonationResourceId,
|
|
1361
|
+
labels,
|
|
1318
1362
|
...limits,
|
|
1319
1363
|
logger,
|
|
1320
1364
|
rateLimit,
|
|
@@ -1557,7 +1601,7 @@ function normalizeInCondition(cond, path) {
|
|
|
1557
1601
|
`Operator "${cond.op}" at ${path} has an empty value list \u2014 pass an array or a comma-separated string with at least one value (e.g. "1,2,3")`
|
|
1558
1602
|
);
|
|
1559
1603
|
}
|
|
1560
|
-
const value = cond.udf === true ? tokens : tokens.map((t) =>
|
|
1604
|
+
const value = cond.udf === true ? tokens : tokens.map((t) => /^(0|[1-9]\d*)$/.test(t) && Number.isSafeInteger(Number(t)) ? Number(t) : t);
|
|
1561
1605
|
return { ...cond, value };
|
|
1562
1606
|
}
|
|
1563
1607
|
function refusedFilterShape(filter) {
|
|
@@ -16808,469 +16852,129 @@ async function toArray(iter) {
|
|
|
16808
16852
|
return out;
|
|
16809
16853
|
}
|
|
16810
16854
|
|
|
16811
|
-
// src/
|
|
16812
|
-
var
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
* existence is asserted by the live-smoke preset check (the AC test).
|
|
16820
|
-
*/
|
|
16821
|
-
billing_join: {
|
|
16822
|
-
resource: "billing_items",
|
|
16823
|
-
fields: [
|
|
16824
|
-
"companyID",
|
|
16825
|
-
"ticketID",
|
|
16826
|
-
"projectID",
|
|
16827
|
-
"timeEntryID",
|
|
16828
|
-
"contractServiceID",
|
|
16829
|
-
"contractID",
|
|
16830
|
-
"billingCodeID",
|
|
16831
|
-
"expenseItemID",
|
|
16832
|
-
"itemDate",
|
|
16833
|
-
"quantity",
|
|
16834
|
-
"rate",
|
|
16835
|
-
"extendedPrice",
|
|
16836
|
-
"totalAmount",
|
|
16837
|
-
"nonBillable"
|
|
16838
|
-
]
|
|
16839
|
-
}
|
|
16840
|
-
};
|
|
16841
|
-
var FIELD_PRESET_NAMES = Object.keys(FIELD_PRESETS);
|
|
16842
|
-
function resolveFieldsPreset(name, resourceKey, explicitIncludeFields) {
|
|
16843
|
-
if (explicitIncludeFields !== void 0 && explicitIncludeFields !== null && explicitIncludeFields.length > 0) {
|
|
16844
|
-
throw new AutotaskConfigError(
|
|
16845
|
-
`fieldsPreset: "${name}" cannot be combined with an explicit includeFields \u2014 the bag is closed (pass ONE of the two: the preset name OR the field list)`
|
|
16846
|
-
);
|
|
16847
|
-
}
|
|
16848
|
-
const preset = Object.hasOwn(FIELD_PRESETS, name) ? FIELD_PRESETS[name] : void 0;
|
|
16849
|
-
if (preset === void 0) {
|
|
16850
|
-
throw new AutotaskConfigError(
|
|
16851
|
-
`fieldsPreset: unknown preset "${name}" \u2014 the preset list is closed (valid names: ${FIELD_PRESET_NAMES.join(", ")})`
|
|
16852
|
-
);
|
|
16853
|
-
}
|
|
16854
|
-
if (preset.resource !== resourceKey) {
|
|
16855
|
-
throw new AutotaskConfigError(
|
|
16856
|
-
`fieldsPreset: "${name}" is a ${preset.resource} preset \u2014 it cannot be resolved on ${resourceKey} (the preset names its resource; the valid names are: ${FIELD_PRESET_NAMES.join(", ")})`
|
|
16857
|
-
);
|
|
16858
|
-
}
|
|
16859
|
-
return [...preset.fields];
|
|
16860
|
-
}
|
|
16855
|
+
// src/operations/types.ts
|
|
16856
|
+
var WORKFLOW_RESOURCES = [
|
|
16857
|
+
"companies",
|
|
16858
|
+
"tickets",
|
|
16859
|
+
"contacts",
|
|
16860
|
+
"configuration_items",
|
|
16861
|
+
"projects"
|
|
16862
|
+
];
|
|
16861
16863
|
|
|
16862
|
-
// src/
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
const
|
|
16869
|
-
const
|
|
16870
|
-
const
|
|
16871
|
-
const
|
|
16872
|
-
|
|
16873
|
-
}
|
|
16874
|
-
function defaultMarkerToken(data) {
|
|
16875
|
-
for (const field of ["name", "title"]) {
|
|
16876
|
-
const v = data[field];
|
|
16877
|
-
if (typeof v === "string" && v.trim() !== "") return v.trim();
|
|
16878
|
-
}
|
|
16879
|
-
return null;
|
|
16880
|
-
}
|
|
16881
|
-
var CONTEXT_ABSENT_MARKER = "#context-absent";
|
|
16882
|
-
function contextAbsentKey(path) {
|
|
16883
|
-
return path + "\0context-absent";
|
|
16884
|
-
}
|
|
16885
|
-
function noteCharsTruncationMarker(charsRemoved) {
|
|
16886
|
-
return `\u2026 [+${charsRemoved} chars truncated]`;
|
|
16887
|
-
}
|
|
16888
|
-
function truncateNoteChars(items, cap, fields) {
|
|
16889
|
-
let truncated = 0;
|
|
16890
|
-
const out = items.map((item) => {
|
|
16891
|
-
if (!isRecord2(item)) return item;
|
|
16892
|
-
let changed = false;
|
|
16893
|
-
const copy = { ...item };
|
|
16894
|
-
for (const field of fields) {
|
|
16895
|
-
const v = copy[field];
|
|
16896
|
-
if (typeof v === "string" && v.length > cap) {
|
|
16897
|
-
const end = cap > 0 && (v.charCodeAt(cap - 1) & 64512) === 55296 ? cap - 1 : cap;
|
|
16898
|
-
copy[field] = v.slice(0, end) + noteCharsTruncationMarker(v.length - end);
|
|
16899
|
-
truncated += 1;
|
|
16900
|
-
changed = true;
|
|
16901
|
-
}
|
|
16902
|
-
}
|
|
16903
|
-
return changed ? copy : item;
|
|
16904
|
-
});
|
|
16905
|
-
return [out, truncated];
|
|
16906
|
-
}
|
|
16907
|
-
function blindEmptyReason(stamp) {
|
|
16864
|
+
// src/operations/fieldCatalog.ts
|
|
16865
|
+
var MAX_FIELD_CATALOG_WIRE = 60;
|
|
16866
|
+
var FIELD_CATALOG_HONESTY_NOTE = "Live field metadata reports the SHAPE of a field (existence, requiredness, type, maxLength, read-only, queryable, picklist-ness) as FLAGS and NAMES: picklist-ness as a flag (isPickList) and picklist field names (picklistFields). With the opt-in `labels: true`, picklist rows carry the vendor's exposed picklist VALUES (labels next to each value); without it the op serves no VALUES. It cannot resolve a picklist VALUE the vendor does not expose to this API user (the W-A4 wall \u2014 e.g. the attachments Type picklist): retrying the catalog will not surface a withheld value; that gap is vendor-side, not a client defect.";
|
|
16867
|
+
var MIN_FIELD_CATALOG_MAX_BYTES = 4096;
|
|
16868
|
+
var FIELD_CATALOG_OPT_KEYS = ["resources", "include", "fresh", "fields", "labels", "compact", "maxBytes"];
|
|
16869
|
+
function projectFieldRow(raw, labels = false, udf = false) {
|
|
16870
|
+
const r = typeof raw === "object" && raw !== null ? raw : {};
|
|
16871
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
16872
|
+
const type = typeof r.dataType === "string" ? r.dataType : typeof r.type === "string" ? r.type : "";
|
|
16873
|
+
const values = labels && r.isPickList === true && Array.isArray(r.picklistValues) && r.picklistValues.length > 0 ? r.picklistValues : void 0;
|
|
16874
|
+
const referenceEntityType = typeof r.referenceEntityType === "string" ? r.referenceEntityType : "";
|
|
16908
16875
|
return {
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16876
|
+
name,
|
|
16877
|
+
...typeof r.label === "string" && r.label !== "" ? { label: r.label } : {},
|
|
16878
|
+
type,
|
|
16879
|
+
isRequired: r.isRequired === true,
|
|
16880
|
+
isReadOnly: r.isReadOnly === true,
|
|
16881
|
+
// Issue #219 N7: a UDF row never serves the vendor's isQueryable (it is
|
|
16882
|
+
// true for UDFs while a plain filter on a UDF name is refused); it serves
|
|
16883
|
+
// the marker the filter actually needs.
|
|
16884
|
+
...udf ? { filterable: "udf-marker" } : { isQueryable: r.isQueryable === true },
|
|
16885
|
+
isPickList: r.isPickList === true,
|
|
16886
|
+
...isObjectOrArrayType(type) ? { writeShape: "replace-all" } : {},
|
|
16887
|
+
maxLength: typeof r.length === "number" ? r.length : null,
|
|
16888
|
+
referenceEntityType,
|
|
16889
|
+
...values !== void 0 ? { picklistValues: values } : {}
|
|
16914
16890
|
};
|
|
16915
16891
|
}
|
|
16916
|
-
function
|
|
16917
|
-
const pinned = EMPTY_REASON_STAMPS[key]?.update;
|
|
16918
|
-
if (pinned === void 0) return;
|
|
16919
|
-
err.details = { ...err.details ?? {}, emptyReason: pinned };
|
|
16920
|
-
}
|
|
16921
|
-
function buildDryRunResult(spec, policy = { redactPersonalData: true }) {
|
|
16922
|
-
if (spec.operation === "") {
|
|
16923
|
-
throw new AutotaskConfigError("dry-run: the operation name is required");
|
|
16924
|
-
}
|
|
16925
|
-
const wouldApply = spec.checks.every((check) => check.ok);
|
|
16926
|
-
const resource = spec.operation.split(".")[0] ?? spec.operation;
|
|
16927
|
-
const diffPart = spec.diff !== void 0 ? {
|
|
16928
|
-
diff: spec.diff.map((entry) => {
|
|
16929
|
-
if (entry.to === void 0) return entry;
|
|
16930
|
-
const to = isRedactedFieldName(entry.field, policy) ? REDACTED : redactResponseBody(entry.to, policy);
|
|
16931
|
-
return { ...entry, to };
|
|
16932
|
-
})
|
|
16933
|
-
} : {};
|
|
16934
|
-
const dataPart = spec.data !== void 0 ? { data: redactResponseBody(spec.data, policy) } : {};
|
|
16892
|
+
function compactFieldRow(f, udf, keepValues) {
|
|
16935
16893
|
return {
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
16941
|
-
|
|
16942
|
-
impact: spec.impact,
|
|
16943
|
-
simulated: true,
|
|
16944
|
-
warnings: spec.warnings ?? [],
|
|
16945
|
-
...dataPart,
|
|
16946
|
-
...spec.emptyReason !== void 0 ? { emptyReason: spec.emptyReason } : {}
|
|
16894
|
+
name: f.name,
|
|
16895
|
+
type: f.type,
|
|
16896
|
+
isPickList: f.isPickList,
|
|
16897
|
+
isReference: f.referenceEntityType !== "",
|
|
16898
|
+
udf,
|
|
16899
|
+
...keepValues && f.picklistValues !== void 0 ? { picklistValues: f.picklistValues } : {}
|
|
16947
16900
|
};
|
|
16948
16901
|
}
|
|
16949
|
-
function
|
|
16950
|
-
const
|
|
16951
|
-
|
|
16952
|
-
|
|
16953
|
-
|
|
16954
|
-
|
|
16955
|
-
|
|
16902
|
+
function isObjectOrArrayType(type) {
|
|
16903
|
+
const t = type.trim().toLowerCase();
|
|
16904
|
+
return t === "object" || t === "array" || t === "collection" || t.endsWith("[]");
|
|
16905
|
+
}
|
|
16906
|
+
function exposesPicklistValues(raw) {
|
|
16907
|
+
const r = typeof raw === "object" && raw !== null ? raw : {};
|
|
16908
|
+
return Array.isArray(r.picklistValues) && r.picklistValues.length > 0;
|
|
16909
|
+
}
|
|
16910
|
+
function applyFieldsFilter(projected, filter, row) {
|
|
16911
|
+
const kept = filter === void 0 ? projected : projected.filter((f) => filter.includes(f.name));
|
|
16912
|
+
if (filter !== void 0) {
|
|
16913
|
+
const present = new Set(projected.map((f) => f.name));
|
|
16914
|
+
row.unmatchedFields = filter.filter((n) => !present.has(n));
|
|
16915
|
+
row.projected = true;
|
|
16956
16916
|
}
|
|
16957
|
-
|
|
16917
|
+
row.fields = kept;
|
|
16918
|
+
row.required = kept.filter((f) => f.isRequired).map((f) => f.name);
|
|
16919
|
+
row.readOnly = kept.filter((f) => f.isReadOnly).map((f) => f.name);
|
|
16920
|
+
row.queryable = kept.filter((f) => f.isQueryable).map((f) => f.name);
|
|
16921
|
+
row.picklistFields = kept.filter((f) => f.isPickList).map((f) => f.name);
|
|
16958
16922
|
}
|
|
16959
|
-
function
|
|
16960
|
-
const
|
|
16961
|
-
|
|
16962
|
-
|
|
16963
|
-
|
|
16923
|
+
function capabilitySets() {
|
|
16924
|
+
const fieldOps = /* @__PURE__ */ new Map();
|
|
16925
|
+
const udfCapable = /* @__PURE__ */ new Set();
|
|
16926
|
+
const resources = /* @__PURE__ */ new Set();
|
|
16927
|
+
for (const rec of CAPABILITIES) {
|
|
16928
|
+
if (rec.resource === "operations") continue;
|
|
16929
|
+
resources.add(rec.resource);
|
|
16930
|
+
if (rec.operation === "getFieldInfo") fieldOps.set(rec.resource, rec);
|
|
16931
|
+
if (rec.udfCapable) udfCapable.add(rec.resource);
|
|
16964
16932
|
}
|
|
16965
|
-
return
|
|
16933
|
+
return { fieldOps, udfCapable, resources };
|
|
16966
16934
|
}
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
16935
|
+
var derivedIndex = null;
|
|
16936
|
+
function derivedFieldsFor(resource) {
|
|
16937
|
+
if (derivedIndex === null) {
|
|
16938
|
+
derivedIndex = /* @__PURE__ */ new Map();
|
|
16939
|
+
for (const rec of CAPABILITIES) {
|
|
16940
|
+
const declared = rec.metadata?.derivedFields;
|
|
16941
|
+
if (!Array.isArray(declared) || derivedIndex.has(rec.resource)) continue;
|
|
16942
|
+
const rows = [];
|
|
16943
|
+
for (const row of declared) {
|
|
16944
|
+
if (typeof row?.field !== "string" || typeof row.rule !== "string") continue;
|
|
16945
|
+
rows.push({
|
|
16946
|
+
field: row.field,
|
|
16947
|
+
rule: row.rule,
|
|
16948
|
+
...typeof row.writableRoute === "string" ? { writableRoute: row.writableRoute } : {}
|
|
16949
|
+
});
|
|
16972
16950
|
}
|
|
16973
|
-
|
|
16974
|
-
}
|
|
16975
|
-
}
|
|
16976
|
-
if (isRecord2(value)) {
|
|
16977
|
-
return Object.entries(value).map(([name, entryValue]) => ({ name, value: entryValue }));
|
|
16951
|
+
derivedIndex.set(rec.resource, rows);
|
|
16952
|
+
}
|
|
16978
16953
|
}
|
|
16979
|
-
|
|
16954
|
+
return derivedIndex.get(resource) ?? [];
|
|
16980
16955
|
}
|
|
16981
|
-
function
|
|
16982
|
-
|
|
16983
|
-
if (
|
|
16984
|
-
|
|
16985
|
-
|
|
16956
|
+
function apiEntityFor(rec) {
|
|
16957
|
+
const m = /^GET \/V1\.0\/([^/]+)\/entityInformation\/fields$/.exec(rec.endpoint ?? "");
|
|
16958
|
+
if (m === null) {
|
|
16959
|
+
throw new AutotaskConfigError(
|
|
16960
|
+
`fieldCatalog: the plan's getFieldInfo endpoint for "${rec.resource}" has no /V1.0/<Entity>/entityInformation/fields shape (${rec.endpoint}) \u2014 plan defect`,
|
|
16961
|
+
{ operation: "operations.fieldCatalog" }
|
|
16962
|
+
);
|
|
16963
|
+
}
|
|
16964
|
+
return m[1];
|
|
16986
16965
|
}
|
|
16987
|
-
function
|
|
16988
|
-
if (
|
|
16989
|
-
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
);
|
|
17000
|
-
}
|
|
17001
|
-
var DEPLURAL_EXCEPTIONS = {
|
|
17002
|
-
tag_aliase: "tag_alias"
|
|
17003
|
-
};
|
|
17004
|
-
function entityNameFromPath(resourcePath) {
|
|
17005
|
-
const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
|
|
17006
|
-
const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
|
|
17007
|
-
if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
|
|
17008
|
-
if (snake.endsWith("s")) {
|
|
17009
|
-
const stripped = snake.slice(0, -1);
|
|
17010
|
-
return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
|
|
17011
|
-
}
|
|
17012
|
-
return snake;
|
|
17013
|
-
}
|
|
17014
|
-
|
|
17015
|
-
// src/operations/expandResourceNames.ts
|
|
17016
|
-
var EXPAND_RESOURCE_NAMES_MAX_IDS = 500;
|
|
17017
|
-
function composeName(record) {
|
|
17018
|
-
const parts = [];
|
|
17019
|
-
for (const field of ["firstName", "middleName", "lastName"]) {
|
|
17020
|
-
const value = record[field];
|
|
17021
|
-
if (typeof value === "string" && value.trim() !== "") parts.push(value.trim());
|
|
17022
|
-
}
|
|
17023
|
-
return parts.length > 0 ? parts.join(" ") : null;
|
|
17024
|
-
}
|
|
17025
|
-
async function expandResourceNames(client, ids2, opts = {}) {
|
|
17026
|
-
if (opts.maxIds !== void 0 && (!Number.isInteger(opts.maxIds) || opts.maxIds < 1)) {
|
|
17027
|
-
throw new AutotaskConfigError(`expandResourceNames: maxIds must be an integer >= 1 (got ${String(opts.maxIds)})`);
|
|
17028
|
-
}
|
|
17029
|
-
const cap = Math.min(EXPAND_RESOURCE_NAMES_MAX_IDS, opts.maxIds ?? EXPAND_RESOURCE_NAMES_MAX_IDS);
|
|
17030
|
-
if (!Array.isArray(ids2) || ids2.length === 0) {
|
|
17031
|
-
throw new AutotaskConfigError(
|
|
17032
|
-
"expandResourceNames: ids must be a non-empty array of vendor numeric resource ids (a zero-id call is a shape error, not a no-op)",
|
|
17033
|
-
{ suggestedAction: "Pass at least one vendor numeric resource id \u2014 safe integer, or the canonical integer string for ids above 2^53." }
|
|
17034
|
-
);
|
|
17035
|
-
}
|
|
17036
|
-
if (ids2.length > cap) {
|
|
17037
|
-
throw new AutotaskConfigError(
|
|
17038
|
-
`expandResourceNames: ${ids2.length} ids over the ${cap}-id cap (EXPAND_RESOURCE_NAMES_MAX_IDS = ${EXPAND_RESOURCE_NAMES_MAX_IDS}; maxIds may lower, never raise) \u2014 zero ids were fetched (pre-request)`,
|
|
17039
|
-
{ suggestedAction: `Split the call into groups of <= ${cap} ids.` }
|
|
17040
|
-
);
|
|
17041
|
-
}
|
|
17042
|
-
const problems = [];
|
|
17043
|
-
const canonical = [];
|
|
17044
|
-
for (let i = 0; i < ids2.length; i += 1) {
|
|
17045
|
-
const at = `ids[${i}]`;
|
|
17046
|
-
const raw = ids2[i];
|
|
17047
|
-
try {
|
|
17048
|
-
canonical[i] = parseIdentifier(raw, at);
|
|
17049
|
-
} catch {
|
|
17050
|
-
if (typeof raw === "string") {
|
|
17051
|
-
problems.push(`${at}: "${raw}" is not a vendor numeric resource id \u2014 non-numeric string identifiers are operations.resolveAny territory (the resource's findBy* helpers)`);
|
|
17052
|
-
} else if (typeof raw === "number") {
|
|
17053
|
-
problems.push(`${at}: ${raw} is not a non-negative safe integer (a JS number this large is already corrupted \u2014 pass the canonical integer string form)`);
|
|
17054
|
-
} else {
|
|
17055
|
-
problems.push(`${at}: required \u2014 the vendor numeric resource id (safe integer, or the canonical integer string for ids above 2^53)`);
|
|
17056
|
-
}
|
|
17057
|
-
}
|
|
17058
|
-
}
|
|
17059
|
-
if (problems.length > 0) {
|
|
17060
|
-
throw new AutotaskConfigError(`expandResourceNames: ${problems.length} problem(s) found pre-request (zero wire was issued)
|
|
17061
|
-
- ` + problems.join("\n- "), {
|
|
17062
|
-
suggestedAction: "Fix every id above and call again \u2014 the ids are vendor NUMERIC resource ids only (name-to-id resolution is operations.resolveAny territory)."
|
|
17063
|
-
});
|
|
17064
|
-
}
|
|
17065
|
-
const unique = [];
|
|
17066
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17067
|
-
for (const id of canonical) {
|
|
17068
|
-
const k = String(id);
|
|
17069
|
-
if (!seen.has(k)) {
|
|
17070
|
-
seen.add(k);
|
|
17071
|
-
unique.push(id);
|
|
17072
|
-
}
|
|
17073
|
-
}
|
|
17074
|
-
const holder = client.resources;
|
|
17075
|
-
const cache = client.http.readCache;
|
|
17076
|
-
const fresh = opts.fresh === true;
|
|
17077
|
-
const byId = /* @__PURE__ */ new Map();
|
|
17078
|
-
for (const id of unique) {
|
|
17079
|
-
const k = String(id);
|
|
17080
|
-
const absentKey = contextAbsentKey(`/V1.0/Resources/${k}`);
|
|
17081
|
-
if (cache !== void 0 && !fresh && cache.get(absentKey) !== void 0) {
|
|
17082
|
-
byId.set(k, { name: null, blind: true });
|
|
17083
|
-
continue;
|
|
17084
|
-
}
|
|
17085
|
-
const epoch = cache?.epoch ?? 0;
|
|
17086
|
-
try {
|
|
17087
|
-
const record = await holder.get(id, { fresh });
|
|
17088
|
-
cache?.delete(absentKey);
|
|
17089
|
-
byId.set(k, { name: composeName(record), blind: false });
|
|
17090
|
-
} catch (err) {
|
|
17091
|
-
if (err instanceof NotFoundError) {
|
|
17092
|
-
cache?.set(absentKey, CONTEXT_ABSENT_MARKER, epoch);
|
|
17093
|
-
byId.set(k, { name: null, blind: true });
|
|
17094
|
-
continue;
|
|
17095
|
-
}
|
|
17096
|
-
if (isAutotaskError(err) && err.code !== "ABORTED") {
|
|
17097
|
-
const e = { code: err.code, message: err.message };
|
|
17098
|
-
if (err.httpStatus !== void 0) e.httpStatus = err.httpStatus;
|
|
17099
|
-
byId.set(k, { name: null, blind: false, error: e });
|
|
17100
|
-
continue;
|
|
17101
|
-
}
|
|
17102
|
-
throw err;
|
|
17103
|
-
}
|
|
17104
|
-
}
|
|
17105
|
-
const results = [];
|
|
17106
|
-
for (const id of canonical) {
|
|
17107
|
-
const out = byId.get(String(id));
|
|
17108
|
-
results.push({ id, name: out.name, blind: out.blind, ...out.error !== void 0 ? { error: out.error } : {} });
|
|
17109
|
-
}
|
|
17110
|
-
const resolved = results.filter((r) => r.name !== null).length;
|
|
17111
|
-
const blind = results.filter((r) => r.blind === true).length;
|
|
17112
|
-
const failed = results.filter((r) => r.error !== void 0).length;
|
|
17113
|
-
return { results, resolved, blind, failed };
|
|
17114
|
-
}
|
|
17115
|
-
|
|
17116
|
-
// src/operations/entityType.ts
|
|
17117
|
-
function buildEntityTypeMap() {
|
|
17118
|
-
const exact = {};
|
|
17119
|
-
for (const [resource, entry] of Object.entries(RELATIONSHIP_MAP)) {
|
|
17120
|
-
const apiEntity = entry.apiEntity;
|
|
17121
|
-
if (typeof apiEntity !== "string" || apiEntity === "") continue;
|
|
17122
|
-
if (exact[apiEntity] === void 0) exact[apiEntity] = resource;
|
|
17123
|
-
}
|
|
17124
|
-
const aliases = {};
|
|
17125
|
-
for (const [apiEntity, resource] of Object.entries(exact)) {
|
|
17126
|
-
if (!apiEntity.endsWith("s") || apiEntity.endsWith("ss")) continue;
|
|
17127
|
-
const singular = apiEntity.slice(0, -1);
|
|
17128
|
-
if (exact[singular] !== void 0 || aliases[singular] !== void 0) continue;
|
|
17129
|
-
aliases[singular] = resource;
|
|
17130
|
-
}
|
|
17131
|
-
return { ...exact, ...aliases };
|
|
17132
|
-
}
|
|
17133
|
-
var ENTITY_TYPE_TO_RESOURCE = buildEntityTypeMap();
|
|
17134
|
-
var ENUMERABLE_RESOURCES = new Set(
|
|
17135
|
-
CAPABILITIES.filter((rec) => rec.resource !== "operations" && (rec.operation === "query" || rec.operation === "get")).map(
|
|
17136
|
-
(rec) => rec.resource
|
|
17137
|
-
)
|
|
17138
|
-
);
|
|
17139
|
-
function resourceForEntityType(entityType) {
|
|
17140
|
-
if (typeof entityType !== "string" || entityType === "") return null;
|
|
17141
|
-
return ENTITY_TYPE_TO_RESOURCE[entityType] ?? null;
|
|
17142
|
-
}
|
|
17143
|
-
function isEnumerableResource(resource) {
|
|
17144
|
-
if (typeof resource !== "string" || resource === "") return false;
|
|
17145
|
-
return ENUMERABLE_RESOURCES.has(resource);
|
|
17146
|
-
}
|
|
17147
|
-
|
|
17148
|
-
// src/operations/types.ts
|
|
17149
|
-
var WORKFLOW_RESOURCES = [
|
|
17150
|
-
"companies",
|
|
17151
|
-
"tickets",
|
|
17152
|
-
"contacts",
|
|
17153
|
-
"configuration_items",
|
|
17154
|
-
"projects"
|
|
17155
|
-
];
|
|
17156
|
-
|
|
17157
|
-
// src/operations/fieldCatalog.ts
|
|
17158
|
-
var MAX_FIELD_CATALOG_WIRE = 60;
|
|
17159
|
-
var FIELD_CATALOG_HONESTY_NOTE = "Live field metadata reports the SHAPE of a field (existence, requiredness, type, maxLength, read-only, queryable, picklist-ness) as FLAGS and NAMES: picklist-ness as a flag (isPickList) and picklist field names (picklistFields). With the opt-in `labels: true`, picklist rows carry the vendor's exposed picklist VALUES (labels next to each value); without it the op serves no VALUES. It cannot resolve a picklist VALUE the vendor does not expose to this API user (the W-A4 wall \u2014 e.g. the attachments Type picklist): retrying the catalog will not surface a withheld value; that gap is vendor-side, not a client defect.";
|
|
17160
|
-
var MIN_FIELD_CATALOG_MAX_BYTES = 4096;
|
|
17161
|
-
var FIELD_CATALOG_OPT_KEYS = ["resources", "include", "fresh", "fields", "labels", "compact", "maxBytes"];
|
|
17162
|
-
function projectFieldRow(raw, labels = false, udf = false) {
|
|
17163
|
-
const r = typeof raw === "object" && raw !== null ? raw : {};
|
|
17164
|
-
const name = typeof r.name === "string" ? r.name : "";
|
|
17165
|
-
const type = typeof r.dataType === "string" ? r.dataType : typeof r.type === "string" ? r.type : "";
|
|
17166
|
-
const values = labels && r.isPickList === true && Array.isArray(r.picklistValues) && r.picklistValues.length > 0 ? r.picklistValues : void 0;
|
|
17167
|
-
const referenceEntityType = typeof r.referenceEntityType === "string" ? r.referenceEntityType : "";
|
|
17168
|
-
const targetResource = resourceForEntityType(referenceEntityType);
|
|
17169
|
-
return {
|
|
17170
|
-
name,
|
|
17171
|
-
...typeof r.label === "string" && r.label !== "" ? { label: r.label } : {},
|
|
17172
|
-
type,
|
|
17173
|
-
isRequired: r.isRequired === true,
|
|
17174
|
-
isReadOnly: r.isReadOnly === true,
|
|
17175
|
-
// Issue #219 N7: a UDF row never serves the vendor's isQueryable (it is
|
|
17176
|
-
// true for UDFs while a plain filter on a UDF name is refused); it serves
|
|
17177
|
-
// the marker the filter actually needs.
|
|
17178
|
-
...udf ? { filterable: "udf-marker" } : { isQueryable: r.isQueryable === true },
|
|
17179
|
-
isPickList: r.isPickList === true,
|
|
17180
|
-
...isObjectOrArrayType(type) ? { writeShape: "replace-all" } : {},
|
|
17181
|
-
maxLength: typeof r.length === "number" ? r.length : null,
|
|
17182
|
-
referenceEntityType,
|
|
17183
|
-
targetResource,
|
|
17184
|
-
targetEnumerable: isEnumerableResource(targetResource),
|
|
17185
|
-
...values !== void 0 ? { picklistValues: values } : {}
|
|
17186
|
-
};
|
|
17187
|
-
}
|
|
17188
|
-
function compactFieldRow(f, udf, keepValues) {
|
|
17189
|
-
return {
|
|
17190
|
-
name: f.name,
|
|
17191
|
-
type: f.type,
|
|
17192
|
-
isPickList: f.isPickList,
|
|
17193
|
-
isReference: f.referenceEntityType !== "",
|
|
17194
|
-
udf,
|
|
17195
|
-
...keepValues && f.picklistValues !== void 0 ? { picklistValues: f.picklistValues } : {}
|
|
17196
|
-
};
|
|
17197
|
-
}
|
|
17198
|
-
function isObjectOrArrayType(type) {
|
|
17199
|
-
const t = type.trim().toLowerCase();
|
|
17200
|
-
return t === "object" || t === "array" || t === "collection" || t.endsWith("[]");
|
|
17201
|
-
}
|
|
17202
|
-
function exposesPicklistValues(raw) {
|
|
17203
|
-
const r = typeof raw === "object" && raw !== null ? raw : {};
|
|
17204
|
-
return Array.isArray(r.picklistValues) && r.picklistValues.length > 0;
|
|
17205
|
-
}
|
|
17206
|
-
function applyFieldsFilter(projected, filter, row) {
|
|
17207
|
-
const kept = filter === void 0 ? projected : projected.filter((f) => filter.includes(f.name));
|
|
17208
|
-
if (filter !== void 0) {
|
|
17209
|
-
const present = new Set(projected.map((f) => f.name));
|
|
17210
|
-
row.unmatchedFields = filter.filter((n) => !present.has(n));
|
|
17211
|
-
row.projected = true;
|
|
17212
|
-
}
|
|
17213
|
-
row.fields = kept;
|
|
17214
|
-
row.required = kept.filter((f) => f.isRequired).map((f) => f.name);
|
|
17215
|
-
row.readOnly = kept.filter((f) => f.isReadOnly).map((f) => f.name);
|
|
17216
|
-
row.queryable = kept.filter((f) => f.isQueryable).map((f) => f.name);
|
|
17217
|
-
row.picklistFields = kept.filter((f) => f.isPickList).map((f) => f.name);
|
|
17218
|
-
}
|
|
17219
|
-
function capabilitySets() {
|
|
17220
|
-
const fieldOps = /* @__PURE__ */ new Map();
|
|
17221
|
-
const udfCapable = /* @__PURE__ */ new Set();
|
|
17222
|
-
const resources = /* @__PURE__ */ new Set();
|
|
17223
|
-
for (const rec of CAPABILITIES) {
|
|
17224
|
-
if (rec.resource === "operations") continue;
|
|
17225
|
-
resources.add(rec.resource);
|
|
17226
|
-
if (rec.operation === "getFieldInfo") fieldOps.set(rec.resource, rec);
|
|
17227
|
-
if (rec.udfCapable) udfCapable.add(rec.resource);
|
|
17228
|
-
}
|
|
17229
|
-
return { fieldOps, udfCapable, resources };
|
|
17230
|
-
}
|
|
17231
|
-
var derivedIndex = null;
|
|
17232
|
-
function derivedFieldsFor(resource) {
|
|
17233
|
-
if (derivedIndex === null) {
|
|
17234
|
-
derivedIndex = /* @__PURE__ */ new Map();
|
|
17235
|
-
for (const rec of CAPABILITIES) {
|
|
17236
|
-
const declared = rec.metadata?.derivedFields;
|
|
17237
|
-
if (!Array.isArray(declared) || derivedIndex.has(rec.resource)) continue;
|
|
17238
|
-
const rows = [];
|
|
17239
|
-
for (const row of declared) {
|
|
17240
|
-
if (typeof row?.field !== "string" || typeof row.rule !== "string") continue;
|
|
17241
|
-
rows.push({
|
|
17242
|
-
field: row.field,
|
|
17243
|
-
rule: row.rule,
|
|
17244
|
-
...typeof row.writableRoute === "string" ? { writableRoute: row.writableRoute } : {}
|
|
17245
|
-
});
|
|
17246
|
-
}
|
|
17247
|
-
derivedIndex.set(rec.resource, rows);
|
|
17248
|
-
}
|
|
17249
|
-
}
|
|
17250
|
-
return derivedIndex.get(resource) ?? [];
|
|
17251
|
-
}
|
|
17252
|
-
function apiEntityFor(rec) {
|
|
17253
|
-
const m = /^GET \/V1\.0\/([^/]+)\/entityInformation\/fields$/.exec(rec.endpoint ?? "");
|
|
17254
|
-
if (m === null) {
|
|
17255
|
-
throw new AutotaskConfigError(
|
|
17256
|
-
`fieldCatalog: the plan's getFieldInfo endpoint for "${rec.resource}" has no /V1.0/<Entity>/entityInformation/fields shape (${rec.endpoint}) \u2014 plan defect`,
|
|
17257
|
-
{ operation: "operations.fieldCatalog" }
|
|
17258
|
-
);
|
|
17259
|
-
}
|
|
17260
|
-
return m[1];
|
|
17261
|
-
}
|
|
17262
|
-
function validateCatalogOptsShape(opts) {
|
|
17263
|
-
if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
|
|
17264
|
-
throw new AutotaskConfigError(
|
|
17265
|
-
"fieldCatalog: opts must be an object { resources?: string[], include?: (info|fields|udf)[], fresh?: boolean, fields?: string[], labels?: boolean, compact?: boolean, maxBytes?: number }"
|
|
17266
|
-
);
|
|
17267
|
-
}
|
|
17268
|
-
for (const key of Object.keys(opts ?? {})) {
|
|
17269
|
-
if (!FIELD_CATALOG_OPT_KEYS.includes(key)) {
|
|
17270
|
-
throw new AutotaskConfigError(
|
|
17271
|
-
`fieldCatalog: unknown opts key "${key}" \u2014 the bag is closed: ${FIELD_CATALOG_OPT_KEYS.join(", ")}`,
|
|
17272
|
-
{ suggestedAction: "Drop the unknown key (autotask_describe(operations.fieldCatalog) shows the exact bag)." }
|
|
17273
|
-
);
|
|
16966
|
+
function validateCatalogOptsShape(opts) {
|
|
16967
|
+
if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
|
|
16968
|
+
throw new AutotaskConfigError(
|
|
16969
|
+
"fieldCatalog: opts must be an object { resources?: string[], include?: (info|fields|udf)[], fresh?: boolean, fields?: string[], labels?: boolean, compact?: boolean, maxBytes?: number }"
|
|
16970
|
+
);
|
|
16971
|
+
}
|
|
16972
|
+
for (const key of Object.keys(opts ?? {})) {
|
|
16973
|
+
if (!FIELD_CATALOG_OPT_KEYS.includes(key)) {
|
|
16974
|
+
throw new AutotaskConfigError(
|
|
16975
|
+
`fieldCatalog: unknown opts key "${key}" \u2014 the bag is closed: ${FIELD_CATALOG_OPT_KEYS.join(", ")}`,
|
|
16976
|
+
{ suggestedAction: "Drop the unknown key (autotask_describe(operations.fieldCatalog) shows the exact bag)." }
|
|
16977
|
+
);
|
|
17274
16978
|
}
|
|
17275
16979
|
}
|
|
17276
16980
|
}
|
|
@@ -17361,7 +17065,8 @@ function resolveSizeOpts(opts) {
|
|
|
17361
17065
|
`fieldCatalog: opts.maxBytes must be an integer >= ${MIN_FIELD_CATALOG_MAX_BYTES} (got ${String(maxBytes)})`
|
|
17362
17066
|
);
|
|
17363
17067
|
}
|
|
17364
|
-
|
|
17068
|
+
const compact = opts?.compact ?? (opts?.fields === void 0 && opts?.labels !== true);
|
|
17069
|
+
return { compact, maxBytes };
|
|
17365
17070
|
}
|
|
17366
17071
|
var jsonBytes = (v) => Buffer.byteLength(JSON.stringify(v), "utf8");
|
|
17367
17072
|
var TRIM_KINDS = ["udf", "fields", "picklistFields", "queryable", "readOnly", "required"];
|
|
@@ -17552,15 +17257,592 @@ async function fieldCatalog(client, opts) {
|
|
|
17552
17257
|
return result;
|
|
17553
17258
|
}
|
|
17554
17259
|
|
|
17555
|
-
// src/
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
"
|
|
17563
|
-
|
|
17260
|
+
// src/resources/base/helpers.ts
|
|
17261
|
+
function describeResponseShape(body) {
|
|
17262
|
+
if (body === null) return "null";
|
|
17263
|
+
if (Array.isArray(body)) return `array(len=${body.length})`;
|
|
17264
|
+
if (typeof body !== "object") return typeof body;
|
|
17265
|
+
const rec = body;
|
|
17266
|
+
const keys = Object.keys(rec).sort();
|
|
17267
|
+
const typeOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
|
|
17268
|
+
const parts = keys.slice(0, 16).map((k) => `${k}:${typeOf(rec[k])}`);
|
|
17269
|
+
const more = keys.length > 16 ? `, +${keys.length - 16} more` : "";
|
|
17270
|
+
return `{ ${parts.join(", ")}${more} }`;
|
|
17271
|
+
}
|
|
17272
|
+
function defaultMarkerToken(data) {
|
|
17273
|
+
for (const field of ["name", "title"]) {
|
|
17274
|
+
const v = data[field];
|
|
17275
|
+
if (typeof v === "string" && v.trim() !== "") return v.trim();
|
|
17276
|
+
}
|
|
17277
|
+
return null;
|
|
17278
|
+
}
|
|
17279
|
+
var CONTEXT_ABSENT_MARKER = "#context-absent";
|
|
17280
|
+
function contextAbsentKey(path) {
|
|
17281
|
+
return path + "\0context-absent";
|
|
17282
|
+
}
|
|
17283
|
+
function noteCharsTruncationMarker(charsRemoved) {
|
|
17284
|
+
return `\u2026 [+${charsRemoved} chars truncated]`;
|
|
17285
|
+
}
|
|
17286
|
+
function truncateNoteChars(items, cap, fields) {
|
|
17287
|
+
let truncated = 0;
|
|
17288
|
+
const out = items.map((item) => {
|
|
17289
|
+
if (!isRecord2(item)) return item;
|
|
17290
|
+
let changed = false;
|
|
17291
|
+
const copy = { ...item };
|
|
17292
|
+
for (const field of fields) {
|
|
17293
|
+
const v = copy[field];
|
|
17294
|
+
if (typeof v === "string" && v.length > cap) {
|
|
17295
|
+
const end = cap > 0 && (v.charCodeAt(cap - 1) & 64512) === 55296 ? cap - 1 : cap;
|
|
17296
|
+
copy[field] = v.slice(0, end) + noteCharsTruncationMarker(v.length - end);
|
|
17297
|
+
truncated += 1;
|
|
17298
|
+
changed = true;
|
|
17299
|
+
}
|
|
17300
|
+
}
|
|
17301
|
+
return changed ? copy : item;
|
|
17302
|
+
});
|
|
17303
|
+
return [out, truncated];
|
|
17304
|
+
}
|
|
17305
|
+
function blindEmptyReason(stamp) {
|
|
17306
|
+
return {
|
|
17307
|
+
surface: stamp.surface,
|
|
17308
|
+
kind: "blind-surface",
|
|
17309
|
+
note: stamp.note,
|
|
17310
|
+
authority: stamp.authority,
|
|
17311
|
+
remedy: "the note is the remedy \u2014 the authority op answers for a record"
|
|
17312
|
+
};
|
|
17313
|
+
}
|
|
17314
|
+
function attachUpdateTargetMissReason(err, key) {
|
|
17315
|
+
const pinned = EMPTY_REASON_STAMPS[key]?.update;
|
|
17316
|
+
if (pinned === void 0) return;
|
|
17317
|
+
err.details = { ...err.details ?? {}, emptyReason: pinned };
|
|
17318
|
+
}
|
|
17319
|
+
function buildDryRunResult(spec, policy = { redactPersonalData: true }) {
|
|
17320
|
+
if (spec.operation === "") {
|
|
17321
|
+
throw new AutotaskConfigError("dry-run: the operation name is required");
|
|
17322
|
+
}
|
|
17323
|
+
const wouldApply = spec.checks.every((check) => check.ok);
|
|
17324
|
+
const resource = spec.operation.split(".")[0] ?? spec.operation;
|
|
17325
|
+
const diffPart = spec.diff !== void 0 ? {
|
|
17326
|
+
diff: spec.diff.map((entry) => {
|
|
17327
|
+
if (entry.to === void 0) return entry;
|
|
17328
|
+
const to = isRedactedFieldName(entry.field, policy) ? REDACTED : redactResponseBody(entry.to, policy);
|
|
17329
|
+
return { ...entry, to };
|
|
17330
|
+
})
|
|
17331
|
+
} : {};
|
|
17332
|
+
const dataPart = spec.data !== void 0 ? { data: redactResponseBody(spec.data, policy) } : {};
|
|
17333
|
+
return {
|
|
17334
|
+
operation: spec.operation,
|
|
17335
|
+
wouldApply,
|
|
17336
|
+
target: { resource, ids: spec.ids },
|
|
17337
|
+
request: { method: spec.method, path: spec.path },
|
|
17338
|
+
...diffPart,
|
|
17339
|
+
checks: spec.checks,
|
|
17340
|
+
impact: spec.impact,
|
|
17341
|
+
simulated: true,
|
|
17342
|
+
warnings: spec.warnings ?? [],
|
|
17343
|
+
...dataPart,
|
|
17344
|
+
...spec.emptyReason !== void 0 ? { emptyReason: spec.emptyReason } : {}
|
|
17345
|
+
};
|
|
17346
|
+
}
|
|
17347
|
+
function computeFieldDiff(current, data) {
|
|
17348
|
+
const out = [];
|
|
17349
|
+
for (const [field, to] of Object.entries(data)) {
|
|
17350
|
+
if (field === "id" || to === void 0) continue;
|
|
17351
|
+
const from = current[field];
|
|
17352
|
+
if (from !== void 0 && JSON.stringify(from) === JSON.stringify(to)) continue;
|
|
17353
|
+
out.push({ field, from: from === void 0 ? null : from, to });
|
|
17354
|
+
}
|
|
17355
|
+
return out;
|
|
17356
|
+
}
|
|
17357
|
+
function mergePartialBody(current, data) {
|
|
17358
|
+
const merged = { ...current };
|
|
17359
|
+
for (const [key, value] of Object.entries(data)) {
|
|
17360
|
+
if (value === void 0) continue;
|
|
17361
|
+
merged[key] = value;
|
|
17362
|
+
}
|
|
17363
|
+
return merged;
|
|
17364
|
+
}
|
|
17365
|
+
function normalizeUdfInput(value) {
|
|
17366
|
+
if (Array.isArray(value)) {
|
|
17367
|
+
return value.map((entry) => {
|
|
17368
|
+
if (!isRecord2(entry) || typeof entry.name !== "string" || entry.name === "") {
|
|
17369
|
+
throw new AutotaskConfigError("userDefinedFields array entries must be { name, value } objects");
|
|
17370
|
+
}
|
|
17371
|
+
return { name: entry.name, value: entry.value };
|
|
17372
|
+
});
|
|
17373
|
+
}
|
|
17374
|
+
if (isRecord2(value)) {
|
|
17375
|
+
return Object.entries(value).map(([name, entryValue]) => ({ name, value: entryValue }));
|
|
17376
|
+
}
|
|
17377
|
+
throw new AutotaskConfigError("userDefinedFields must be an array of { name, value } entries or a name-to-value map");
|
|
17378
|
+
}
|
|
17379
|
+
function blindStampFor(stamp, filter) {
|
|
17380
|
+
if (stamp === void 0) return void 0;
|
|
17381
|
+
if (stamp.kind !== "unfiltered-blind") return stamp;
|
|
17382
|
+
const conditions = normalizedConditions(filter);
|
|
17383
|
+
return conditions === void 0 || conditions.length === 0 ? stamp : void 0;
|
|
17384
|
+
}
|
|
17385
|
+
function decodeFileData(body) {
|
|
17386
|
+
if (isRecord2(body)) {
|
|
17387
|
+
const data = body.data;
|
|
17388
|
+
if (typeof data === "string") return Buffer.from(data, "base64");
|
|
17389
|
+
if (Array.isArray(data) && data.every((b) => typeof b === "number")) {
|
|
17390
|
+
return Buffer.from(data);
|
|
17391
|
+
}
|
|
17392
|
+
}
|
|
17393
|
+
if (typeof body === "string") return Buffer.from(body, "base64");
|
|
17394
|
+
throw new AutotaskError(
|
|
17395
|
+
"Expected the file envelope FileQueryResultModel { data } (or a base64 string); got a different shape",
|
|
17396
|
+
{ code: "INVALID_RESPONSE", category: "server" }
|
|
17397
|
+
);
|
|
17398
|
+
}
|
|
17399
|
+
var DEPLURAL_EXCEPTIONS = {
|
|
17400
|
+
tag_aliase: "tag_alias"
|
|
17401
|
+
};
|
|
17402
|
+
function entityNameFromPath(resourcePath) {
|
|
17403
|
+
const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
|
|
17404
|
+
const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
|
|
17405
|
+
if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
|
|
17406
|
+
if (snake.endsWith("s")) {
|
|
17407
|
+
const stripped = snake.slice(0, -1);
|
|
17408
|
+
return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
|
|
17409
|
+
}
|
|
17410
|
+
return snake;
|
|
17411
|
+
}
|
|
17412
|
+
|
|
17413
|
+
// src/operations/expandResourceNames.ts
|
|
17414
|
+
var EXPAND_RESOURCE_NAMES_MAX_IDS = 500;
|
|
17415
|
+
function composeName(record) {
|
|
17416
|
+
const parts = [];
|
|
17417
|
+
for (const field of ["firstName", "middleName", "lastName"]) {
|
|
17418
|
+
const value = record[field];
|
|
17419
|
+
if (typeof value === "string" && value.trim() !== "") parts.push(value.trim());
|
|
17420
|
+
}
|
|
17421
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
17422
|
+
}
|
|
17423
|
+
async function expandResourceNames(client, ids2, opts = {}) {
|
|
17424
|
+
if (opts.maxIds !== void 0 && (!Number.isInteger(opts.maxIds) || opts.maxIds < 1)) {
|
|
17425
|
+
throw new AutotaskConfigError(`expandResourceNames: maxIds must be an integer >= 1 (got ${String(opts.maxIds)})`);
|
|
17426
|
+
}
|
|
17427
|
+
const cap = Math.min(EXPAND_RESOURCE_NAMES_MAX_IDS, opts.maxIds ?? EXPAND_RESOURCE_NAMES_MAX_IDS);
|
|
17428
|
+
if (!Array.isArray(ids2) || ids2.length === 0) {
|
|
17429
|
+
throw new AutotaskConfigError(
|
|
17430
|
+
"expandResourceNames: ids must be a non-empty array of vendor numeric resource ids (a zero-id call is a shape error, not a no-op)",
|
|
17431
|
+
{ suggestedAction: "Pass at least one vendor numeric resource id \u2014 safe integer, or the canonical integer string for ids above 2^53." }
|
|
17432
|
+
);
|
|
17433
|
+
}
|
|
17434
|
+
if (ids2.length > cap) {
|
|
17435
|
+
throw new AutotaskConfigError(
|
|
17436
|
+
`expandResourceNames: ${ids2.length} ids over the ${cap}-id cap (EXPAND_RESOURCE_NAMES_MAX_IDS = ${EXPAND_RESOURCE_NAMES_MAX_IDS}; maxIds may lower, never raise) \u2014 zero ids were fetched (pre-request)`,
|
|
17437
|
+
{ suggestedAction: `Split the call into groups of <= ${cap} ids.` }
|
|
17438
|
+
);
|
|
17439
|
+
}
|
|
17440
|
+
const problems = [];
|
|
17441
|
+
const canonical = [];
|
|
17442
|
+
for (let i = 0; i < ids2.length; i += 1) {
|
|
17443
|
+
const at = `ids[${i}]`;
|
|
17444
|
+
const raw = ids2[i];
|
|
17445
|
+
try {
|
|
17446
|
+
canonical[i] = parseIdentifier(raw, at);
|
|
17447
|
+
} catch {
|
|
17448
|
+
if (typeof raw === "string") {
|
|
17449
|
+
problems.push(`${at}: "${raw}" is not a vendor numeric resource id \u2014 non-numeric string identifiers are operations.resolveAny territory (the resource's findBy* helpers)`);
|
|
17450
|
+
} else if (typeof raw === "number") {
|
|
17451
|
+
problems.push(`${at}: ${raw} is not a non-negative safe integer (a JS number this large is already corrupted \u2014 pass the canonical integer string form)`);
|
|
17452
|
+
} else {
|
|
17453
|
+
problems.push(`${at}: required \u2014 the vendor numeric resource id (safe integer, or the canonical integer string for ids above 2^53)`);
|
|
17454
|
+
}
|
|
17455
|
+
}
|
|
17456
|
+
}
|
|
17457
|
+
if (problems.length > 0) {
|
|
17458
|
+
throw new AutotaskConfigError(`expandResourceNames: ${problems.length} problem(s) found pre-request (zero wire was issued)
|
|
17459
|
+
- ` + problems.join("\n- "), {
|
|
17460
|
+
suggestedAction: "Fix every id above and call again \u2014 the ids are vendor NUMERIC resource ids only (name-to-id resolution is operations.resolveAny territory)."
|
|
17461
|
+
});
|
|
17462
|
+
}
|
|
17463
|
+
const unique = [];
|
|
17464
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17465
|
+
for (const id of canonical) {
|
|
17466
|
+
const k = String(id);
|
|
17467
|
+
if (!seen.has(k)) {
|
|
17468
|
+
seen.add(k);
|
|
17469
|
+
unique.push(id);
|
|
17470
|
+
}
|
|
17471
|
+
}
|
|
17472
|
+
const holder = client.resources;
|
|
17473
|
+
const cache = client.http.readCache;
|
|
17474
|
+
const fresh = opts.fresh === true;
|
|
17475
|
+
const byId = /* @__PURE__ */ new Map();
|
|
17476
|
+
for (const id of unique) {
|
|
17477
|
+
const k = String(id);
|
|
17478
|
+
const absentKey = contextAbsentKey(`/V1.0/Resources/${k}`);
|
|
17479
|
+
if (cache !== void 0 && !fresh && cache.get(absentKey) !== void 0) {
|
|
17480
|
+
byId.set(k, { name: null, blind: true });
|
|
17481
|
+
continue;
|
|
17482
|
+
}
|
|
17483
|
+
const epoch = cache?.epoch ?? 0;
|
|
17484
|
+
try {
|
|
17485
|
+
const record = await holder.get(id, { fresh });
|
|
17486
|
+
cache?.delete(absentKey);
|
|
17487
|
+
byId.set(k, { name: composeName(record), blind: false });
|
|
17488
|
+
} catch (err) {
|
|
17489
|
+
if (err instanceof NotFoundError) {
|
|
17490
|
+
cache?.set(absentKey, CONTEXT_ABSENT_MARKER, epoch);
|
|
17491
|
+
byId.set(k, { name: null, blind: true });
|
|
17492
|
+
continue;
|
|
17493
|
+
}
|
|
17494
|
+
if (isAutotaskError(err) && err.code !== "ABORTED") {
|
|
17495
|
+
const e = { code: err.code, message: err.message };
|
|
17496
|
+
if (err.httpStatus !== void 0) e.httpStatus = err.httpStatus;
|
|
17497
|
+
byId.set(k, { name: null, blind: false, error: e });
|
|
17498
|
+
continue;
|
|
17499
|
+
}
|
|
17500
|
+
throw err;
|
|
17501
|
+
}
|
|
17502
|
+
}
|
|
17503
|
+
const results = [];
|
|
17504
|
+
for (const id of canonical) {
|
|
17505
|
+
const out = byId.get(String(id));
|
|
17506
|
+
results.push({ id, name: out.name, blind: out.blind, ...out.error !== void 0 ? { error: out.error } : {} });
|
|
17507
|
+
}
|
|
17508
|
+
const resolved = results.filter((r) => r.name !== null).length;
|
|
17509
|
+
const blind = results.filter((r) => r.blind === true).length;
|
|
17510
|
+
const failed = results.filter((r) => r.error !== void 0).length;
|
|
17511
|
+
return { results, resolved, blind, failed };
|
|
17512
|
+
}
|
|
17513
|
+
|
|
17514
|
+
// src/operations/lineOfBusinessChain.ts
|
|
17515
|
+
function vendorId(value) {
|
|
17516
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
|
|
17517
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value.trim())) {
|
|
17518
|
+
const n = Number(value.trim());
|
|
17519
|
+
return Number.isSafeInteger(n) && n >= 1 ? n : void 0;
|
|
17520
|
+
}
|
|
17521
|
+
return void 0;
|
|
17522
|
+
}
|
|
17523
|
+
async function resolveLineOfBusinessChain(http, associationId, options) {
|
|
17524
|
+
const operation = options.operation ?? "compositeNames";
|
|
17525
|
+
const degrade = options.onError === "degrade";
|
|
17526
|
+
const get = (path) => {
|
|
17527
|
+
const res = http.request({ method: "GET", path, operation }).then((body) => {
|
|
17528
|
+
const b = body;
|
|
17529
|
+
if (b === null || !("item" in b) || b.item === null || b.item === void 0) return null;
|
|
17530
|
+
return b.item;
|
|
17531
|
+
});
|
|
17532
|
+
if (!degrade) {
|
|
17533
|
+
return res.catch((e) => {
|
|
17534
|
+
if (e instanceof NotFoundError) return null;
|
|
17535
|
+
throw e;
|
|
17536
|
+
});
|
|
17537
|
+
}
|
|
17538
|
+
return res.catch((e) => {
|
|
17539
|
+
if (e instanceof NotFoundError || isAutotaskError(e) && e.code !== "ABORTED") return null;
|
|
17540
|
+
throw e;
|
|
17541
|
+
});
|
|
17542
|
+
};
|
|
17543
|
+
let remaining = options.budget;
|
|
17544
|
+
if (remaining < 1) {
|
|
17545
|
+
return {
|
|
17546
|
+
lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: "capped", organizationalLevel2: "capped", organizationalLevel1: "capped" } },
|
|
17547
|
+
used: 0,
|
|
17548
|
+
truncated: true
|
|
17549
|
+
};
|
|
17550
|
+
}
|
|
17551
|
+
remaining -= 1;
|
|
17552
|
+
const association = await get(`/V1.0/OrganizationalLevelAssociations/${associationId}`);
|
|
17553
|
+
const associationMarker = association === null ? "unresolved" : "ok";
|
|
17554
|
+
if (association === null) {
|
|
17555
|
+
return {
|
|
17556
|
+
lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: associationMarker, organizationalLevel2: "missing", organizationalLevel1: "missing" } },
|
|
17557
|
+
used: 1,
|
|
17558
|
+
truncated: false
|
|
17559
|
+
};
|
|
17560
|
+
}
|
|
17561
|
+
const l2Id = vendorId(association.organizationalLevel2ID);
|
|
17562
|
+
const l1Id = vendorId(association.organizationalLevel1ID);
|
|
17563
|
+
let l2Marker = l2Id !== void 0 ? "unresolved" : "missing";
|
|
17564
|
+
let l1Marker = l1Id !== void 0 ? "unresolved" : "missing";
|
|
17565
|
+
const need = (l2Id !== void 0 ? 1 : 0) + (l1Id !== void 0 ? 1 : 0);
|
|
17566
|
+
let l2 = null;
|
|
17567
|
+
let l1 = null;
|
|
17568
|
+
let used = 1;
|
|
17569
|
+
let truncated = false;
|
|
17570
|
+
if (need > 0 && remaining < need) {
|
|
17571
|
+
truncated = true;
|
|
17572
|
+
if (l2Id !== void 0) l2Marker = "capped";
|
|
17573
|
+
if (l1Id !== void 0) l1Marker = "capped";
|
|
17574
|
+
} else {
|
|
17575
|
+
used += need;
|
|
17576
|
+
const [r2, r1] = await Promise.all([
|
|
17577
|
+
l2Id !== void 0 ? get(`/V1.0/OrganizationalLevel2s/${l2Id}`) : Promise.resolve(null),
|
|
17578
|
+
l1Id !== void 0 ? get(`/V1.0/OrganizationalLevel1s/${l1Id}`) : Promise.resolve(null)
|
|
17579
|
+
]);
|
|
17580
|
+
l2 = r2;
|
|
17581
|
+
l1 = r1;
|
|
17582
|
+
if (l2Id !== void 0) l2Marker = l2 === null ? "unresolved" : "ok";
|
|
17583
|
+
if (l1Id !== void 0) l1Marker = l1 === null ? "unresolved" : "ok";
|
|
17584
|
+
}
|
|
17585
|
+
return {
|
|
17586
|
+
lineOfBusiness: { association, organizationalLevel2: l2, organizationalLevel1: l1, markers: { association: associationMarker, organizationalLevel2: l2Marker, organizationalLevel1: l1Marker } },
|
|
17587
|
+
used,
|
|
17588
|
+
truncated
|
|
17589
|
+
};
|
|
17590
|
+
}
|
|
17591
|
+
|
|
17592
|
+
// src/operations/compositeNames.ts
|
|
17593
|
+
var PICKLIST_VALUE_NOT_SERVED_NOTE = "The picklist is served to this API user, but this record's value is not in it (an inactive or unexposed value) \u2014 the label cannot be resolved and is null; refreshing the field catalog will not surface it. (A picklist the vendor does not expose at all is reported with the field-catalog honesty note instead.)";
|
|
17594
|
+
var DEFAULT_MAX_NAME_READS = 20;
|
|
17595
|
+
var TICKET_LABEL_FIELDS = ["status", "priority", "ticketType", "issueType", "subIssueType", "source", "queueID"];
|
|
17596
|
+
function isVendorId(value) {
|
|
17597
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0;
|
|
17598
|
+
if (typeof value === "string") return /^\d+$/.test(value);
|
|
17599
|
+
return false;
|
|
17600
|
+
}
|
|
17601
|
+
var zeroIsRecord = (table) => table === "company";
|
|
17602
|
+
function createNamePlan(labelFields = [], labelResource) {
|
|
17603
|
+
const resourceIds = [];
|
|
17604
|
+
const resSeen = /* @__PURE__ */ new Set();
|
|
17605
|
+
const recordFks = [];
|
|
17606
|
+
const fkSeen = /* @__PURE__ */ new Set();
|
|
17607
|
+
let lineOfBusinessId;
|
|
17608
|
+
let lineOfBusinessSource;
|
|
17609
|
+
let lineOfBusinessContractId;
|
|
17610
|
+
return {
|
|
17611
|
+
plan: () => ({
|
|
17612
|
+
resourceIds,
|
|
17613
|
+
recordFks,
|
|
17614
|
+
labelFields,
|
|
17615
|
+
...labelResource !== void 0 ? { labelResource } : {},
|
|
17616
|
+
...lineOfBusinessId !== void 0 ? { lineOfBusinessId } : {},
|
|
17617
|
+
...lineOfBusinessSource !== void 0 ? { lineOfBusinessSource } : {},
|
|
17618
|
+
...lineOfBusinessContractId !== void 0 ? { lineOfBusinessContractId } : {}
|
|
17619
|
+
}),
|
|
17620
|
+
addResource: (value) => {
|
|
17621
|
+
if (!isVendorId(value) || Number(value) === 0) return;
|
|
17622
|
+
const k = String(value);
|
|
17623
|
+
if (!resSeen.has(k)) {
|
|
17624
|
+
resSeen.add(k);
|
|
17625
|
+
resourceIds.push(value);
|
|
17626
|
+
}
|
|
17627
|
+
},
|
|
17628
|
+
addFk: (table, value) => {
|
|
17629
|
+
if (!isVendorId(value) || Number(value) === 0 && !zeroIsRecord(table)) return;
|
|
17630
|
+
const k = `${table}:${String(value)}`;
|
|
17631
|
+
if (!fkSeen.has(k)) {
|
|
17632
|
+
fkSeen.add(k);
|
|
17633
|
+
recordFks.push({ table, id: value });
|
|
17634
|
+
}
|
|
17635
|
+
},
|
|
17636
|
+
addLineOfBusinessFromContract: (value) => {
|
|
17637
|
+
if (!isVendorId(value)) return;
|
|
17638
|
+
lineOfBusinessContractId = value;
|
|
17639
|
+
},
|
|
17640
|
+
addLineOfBusiness: (value, origin) => {
|
|
17641
|
+
if (!isVendorId(value)) return;
|
|
17642
|
+
lineOfBusinessId = value;
|
|
17643
|
+
if (origin !== void 0) {
|
|
17644
|
+
lineOfBusinessSource = origin.source;
|
|
17645
|
+
if (origin.contractId !== void 0) lineOfBusinessContractId = origin.contractId;
|
|
17646
|
+
}
|
|
17647
|
+
}
|
|
17648
|
+
};
|
|
17649
|
+
}
|
|
17650
|
+
function addNameSibling(row, field, resolve) {
|
|
17651
|
+
const value = row[field];
|
|
17652
|
+
if (value === void 0) return;
|
|
17653
|
+
row[`${field}Name`] = isVendorId(value) ? resolve(value) : null;
|
|
17654
|
+
}
|
|
17655
|
+
function addLabelSibling(row, field, names) {
|
|
17656
|
+
const value = row[field];
|
|
17657
|
+
if (value === void 0) return;
|
|
17658
|
+
row[`${field}Label`] = names.labelFor(field, value);
|
|
17659
|
+
}
|
|
17660
|
+
var FK_TABLE_PATH = {
|
|
17661
|
+
role: "/V1.0/Roles",
|
|
17662
|
+
billingCode: "/V1.0/BillingCodes",
|
|
17663
|
+
contract: "/V1.0/Contracts",
|
|
17664
|
+
company: "/V1.0/Companies",
|
|
17665
|
+
ticket: "/V1.0/Tickets"
|
|
17666
|
+
};
|
|
17667
|
+
var FK_NAME_FIELD = {
|
|
17668
|
+
role: "name",
|
|
17669
|
+
billingCode: "name",
|
|
17670
|
+
contract: "contractName",
|
|
17671
|
+
company: "companyName",
|
|
17672
|
+
ticket: "ticketNumber"
|
|
17673
|
+
};
|
|
17674
|
+
function fkName(record, table) {
|
|
17675
|
+
if (table === "ticket") {
|
|
17676
|
+
const parts = [record["ticketNumber"], record["title"]].filter((v) => typeof v === "string" && v.trim() !== "").map((v) => v.trim());
|
|
17677
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
17678
|
+
}
|
|
17679
|
+
const value = record[FK_NAME_FIELD[table]];
|
|
17680
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
17681
|
+
}
|
|
17682
|
+
function faultRow(err) {
|
|
17683
|
+
const e = err;
|
|
17684
|
+
return e.httpStatus !== void 0 ? { code: e.code, message: e.message, httpStatus: e.httpStatus } : { code: e.code, message: e.message };
|
|
17685
|
+
}
|
|
17686
|
+
async function resolveCompositeNames(http, plan, opts = {}) {
|
|
17687
|
+
if (opts.maxNameReads !== void 0 && (!Number.isInteger(opts.maxNameReads) || opts.maxNameReads < 1)) {
|
|
17688
|
+
throw new AutotaskConfigError(`composite names: maxNameReads must be an integer >= 1 (got ${String(opts.maxNameReads)})`);
|
|
17689
|
+
}
|
|
17690
|
+
const budget = Math.min(DEFAULT_MAX_NAME_READS, opts.maxNameReads ?? DEFAULT_MAX_NAME_READS);
|
|
17691
|
+
const client = http.client;
|
|
17692
|
+
if (client === void 0) {
|
|
17693
|
+
throw new AutotaskConfigError("composite names: the transport is not wired to an AutotaskClient (the composite must run through the client)");
|
|
17694
|
+
}
|
|
17695
|
+
const readCache = http.readCache;
|
|
17696
|
+
const section = { truncated: false, rows: {}, reasons: {} };
|
|
17697
|
+
let remaining = budget;
|
|
17698
|
+
let contractLobPointer;
|
|
17699
|
+
let contractLobUnread;
|
|
17700
|
+
const coldResource = [];
|
|
17701
|
+
for (const id of plan.resourceIds) {
|
|
17702
|
+
const path = `/V1.0/Resources/${id}`;
|
|
17703
|
+
if (readCache !== void 0 && (readCache.peek(path).hit || readCache.peek(contextAbsentKey(path)).hit)) continue;
|
|
17704
|
+
coldResource.push(id);
|
|
17705
|
+
}
|
|
17706
|
+
const withinBudget = coldResource.slice(0, remaining);
|
|
17707
|
+
const overBudgetResource = coldResource.slice(remaining);
|
|
17708
|
+
remaining -= withinBudget.length;
|
|
17709
|
+
for (const id of overBudgetResource) {
|
|
17710
|
+
section.rows[`resource:${id}`] = { name: null, blind: false };
|
|
17711
|
+
section.truncated = true;
|
|
17712
|
+
}
|
|
17713
|
+
const overSet = new Set(overBudgetResource.map((id) => String(id)));
|
|
17714
|
+
const opIds = plan.resourceIds.filter((id) => !overSet.has(String(id)));
|
|
17715
|
+
if (opIds.length > 0) {
|
|
17716
|
+
let pass = opIds;
|
|
17717
|
+
if (pass.length > 500) {
|
|
17718
|
+
for (const id of pass.slice(500)) {
|
|
17719
|
+
section.rows[`resource:${id}`] = { name: null, blind: false };
|
|
17720
|
+
section.truncated = true;
|
|
17721
|
+
}
|
|
17722
|
+
pass = pass.slice(0, 500);
|
|
17723
|
+
}
|
|
17724
|
+
const res = await expandResourceNames(client, pass);
|
|
17725
|
+
for (const r of res.results) {
|
|
17726
|
+
const row = { name: r.name, blind: r.blind };
|
|
17727
|
+
if (r.error !== void 0) row.error = faultRow(r.error);
|
|
17728
|
+
section.rows[`resource:${r.id}`] = row;
|
|
17729
|
+
}
|
|
17730
|
+
}
|
|
17731
|
+
const holders = {
|
|
17732
|
+
role: client.roles,
|
|
17733
|
+
billingCode: client.billingCodes,
|
|
17734
|
+
contract: client.contracts,
|
|
17735
|
+
company: client.companies,
|
|
17736
|
+
ticket: client.tickets
|
|
17737
|
+
};
|
|
17738
|
+
for (const fk of plan.recordFks) {
|
|
17739
|
+
const key = `${fk.table}:${fk.id}`;
|
|
17740
|
+
const cold = readCache === void 0 || readCache.get(`${FK_TABLE_PATH[fk.table]}/${fk.id}`) === void 0;
|
|
17741
|
+
if (cold) {
|
|
17742
|
+
if (remaining < 1) {
|
|
17743
|
+
section.rows[key] = { name: null, blind: false };
|
|
17744
|
+
section.truncated = true;
|
|
17745
|
+
if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
|
|
17746
|
+
contractLobUnread = "capped";
|
|
17747
|
+
}
|
|
17748
|
+
continue;
|
|
17749
|
+
}
|
|
17750
|
+
remaining -= 1;
|
|
17751
|
+
}
|
|
17752
|
+
try {
|
|
17753
|
+
const record = await holders[fk.table].get(fk.id);
|
|
17754
|
+
if (fk.table === "contract" && plan.lineOfBusinessId === void 0 && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
|
|
17755
|
+
contractLobPointer = vendorId(record["organizationalLevelAssociationID"]);
|
|
17756
|
+
}
|
|
17757
|
+
section.rows[key] = { name: fkName(record, fk.table), blind: false };
|
|
17758
|
+
} catch (err) {
|
|
17759
|
+
if (err instanceof NotFoundError) {
|
|
17760
|
+
section.rows[key] = { name: null, blind: true };
|
|
17761
|
+
} else if (isAutotaskError(err) && err.code !== "ABORTED") {
|
|
17762
|
+
section.rows[key] = { name: null, blind: false, error: faultRow(err) };
|
|
17763
|
+
if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
|
|
17764
|
+
contractLobUnread = "unresolved";
|
|
17765
|
+
}
|
|
17766
|
+
} else {
|
|
17767
|
+
throw err;
|
|
17768
|
+
}
|
|
17769
|
+
}
|
|
17770
|
+
}
|
|
17771
|
+
const picklists = {};
|
|
17772
|
+
if (plan.labelFields.length > 0) {
|
|
17773
|
+
try {
|
|
17774
|
+
const labelResource = plan.labelResource ?? "tickets";
|
|
17775
|
+
const catalog = await fieldCatalog(client, { resources: [labelResource], labels: true, fields: [...plan.labelFields] });
|
|
17776
|
+
const row = catalog.rows.find((r) => r.resource === labelResource);
|
|
17777
|
+
if (row !== void 0 && row.error === null && row.fields !== void 0) {
|
|
17778
|
+
for (const f of plan.labelFields) {
|
|
17779
|
+
const frow = row.fields.find((x) => x.name === f);
|
|
17780
|
+
picklists[f] = frow?.picklistValues;
|
|
17781
|
+
}
|
|
17782
|
+
}
|
|
17783
|
+
} catch (err) {
|
|
17784
|
+
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
17785
|
+
}
|
|
17786
|
+
}
|
|
17787
|
+
let lineOfBusiness2 = null;
|
|
17788
|
+
const lobId = plan.lineOfBusinessId ?? contractLobPointer;
|
|
17789
|
+
if (lobId !== void 0) {
|
|
17790
|
+
const chain = await resolveLineOfBusinessChain(http, lobId, { budget: remaining, onError: "degrade" });
|
|
17791
|
+
remaining -= chain.used;
|
|
17792
|
+
if (chain.truncated) section.truncated = true;
|
|
17793
|
+
const fromContract = plan.lineOfBusinessId === void 0;
|
|
17794
|
+
lineOfBusiness2 = {
|
|
17795
|
+
...chain.lineOfBusiness,
|
|
17796
|
+
...fromContract ? { source: "contract", contractId: plan.lineOfBusinessContractId } : plan.lineOfBusinessSource !== void 0 ? { source: plan.lineOfBusinessSource } : {},
|
|
17797
|
+
...!fromContract && plan.lineOfBusinessContractId !== void 0 ? { contractId: plan.lineOfBusinessContractId } : {}
|
|
17798
|
+
};
|
|
17799
|
+
} else if (contractLobUnread !== void 0 && plan.lineOfBusinessContractId !== void 0) {
|
|
17800
|
+
lineOfBusiness2 = {
|
|
17801
|
+
source: "contract",
|
|
17802
|
+
contractId: plan.lineOfBusinessContractId,
|
|
17803
|
+
association: null,
|
|
17804
|
+
organizationalLevel2: null,
|
|
17805
|
+
organizationalLevel1: null,
|
|
17806
|
+
markers: { association: contractLobUnread, organizationalLevel2: "missing", organizationalLevel1: "missing" }
|
|
17807
|
+
};
|
|
17808
|
+
}
|
|
17809
|
+
return {
|
|
17810
|
+
section,
|
|
17811
|
+
lineOfBusiness: lineOfBusiness2,
|
|
17812
|
+
resourceName: (id) => section.rows[`resource:${id}`]?.name ?? null,
|
|
17813
|
+
recordName: (table, id) => section.rows[`${table}:${id}`]?.name ?? null,
|
|
17814
|
+
labelFor: (field, value) => {
|
|
17815
|
+
if (value === null || value === void 0) return null;
|
|
17816
|
+
const values = picklists[field];
|
|
17817
|
+
if (values === void 0) {
|
|
17818
|
+
section.reasons[field] = FIELD_CATALOG_HONESTY_NOTE;
|
|
17819
|
+
return null;
|
|
17820
|
+
}
|
|
17821
|
+
const hit = values.find((v) => String(v.value) === String(value));
|
|
17822
|
+
if (hit === void 0) {
|
|
17823
|
+
if (String(value) !== "0") section.reasons[field] = PICKLIST_VALUE_NOT_SERVED_NOTE;
|
|
17824
|
+
return null;
|
|
17825
|
+
}
|
|
17826
|
+
return typeof hit.label === "string" ? hit.label : null;
|
|
17827
|
+
}
|
|
17828
|
+
};
|
|
17829
|
+
}
|
|
17830
|
+
|
|
17831
|
+
// src/resources/base/types.ts
|
|
17832
|
+
var BOUNDED_QUERY_DEFAULT_LIMIT = 500;
|
|
17833
|
+
var BOUNDED_QUERY_MAX_LIMIT = 2500;
|
|
17834
|
+
var LATEST_MAX_COUNT_PROBES = 64;
|
|
17835
|
+
var MARKER_RECOVERY_LIMIT = 5;
|
|
17836
|
+
|
|
17837
|
+
// src/field-sets.ts
|
|
17838
|
+
var MODEL_FIELD_SETS = {
|
|
17839
|
+
"appointments": ["id", "createDateTime", "creatorResourceID", "description", "endDateTime", "resourceID", "startDateTime", "title", "updateDateTime"],
|
|
17840
|
+
"billing_codes": ["id", "afterHoursWorkType", "billingCodeType", "department", "description", "externalNumber", "generalLedgerAccount", "isActive", "isExcludedFromNewContracts", "markupRate", "name", "taxCategoryID", "unitCost", "unitPrice", "useType"],
|
|
17841
|
+
"billing_item_approval_levels": ["id", "approvalDateTime", "approvalLevel", "approvalResourceID", "timeEntryID"],
|
|
17842
|
+
"billing_items": ["id", "accountManagerWhenApprovedID", "billingCodeID", "billingItemType", "companyID", "configurationItemID", "contractBlockID", "contractChargeID", "contractID", "contractServiceAdjustmentID", "contractServiceBundleAdjustmentID", "contractServiceBundleID", "contractServiceBundlePeriodID", "contractServiceID", "contractServicePeriodID", "description", "expenseItemID", "extendedPrice", "internalCurrencyExtendedPrice", "internalCurrencyRate", "internalCurrencyTaxDollars", "internalCurrencyTotalAmount", "invoiceID", "itemApproverID", "itemDate", "itemName", "lineItemFullDescription", "lineItemGroupDescription", "milestoneID", "nonBillable", "organizationalLevelAssociationID", "ourCost", "postedDate", "postedOnTime", "projectChargeID", "projectID", "purchaseOrderNumber", "quantity", "rate", "roleID", "serviceBundleID", "serviceID", "sortOrderID", "subType", "taskID", "taxDollars", "ticketChargeID", "ticketID", "timeEntryID", "totalAmount", "vendorID", "webServiceDate"],
|
|
17843
|
+
"change_request_links": ["id", "changeRequestTicketID", "problemOrIncidentTicketID"],
|
|
17844
|
+
"checklist_libraries": ["id", "description", "entityType", "isActive", "name"],
|
|
17845
|
+
"checklist_library_checklist_items": ["id", "checklistLibraryID", "isImportant", "itemName", "knowledgebaseArticleID", "position", "soapParentPropertyId"],
|
|
17564
17846
|
"classification_icons": ["id", "description", "isActive", "isSystem", "name", "relativeUrl"],
|
|
17565
17847
|
"companies": ["id", "additionalAddressInformation", "address1", "address2", "alternatePhone1", "alternatePhone2", "apiVendorID", "assetValue", "billToCompanyLocationID", "billToAdditionalAddressInformation", "billingAddress1", "billingAddress2", "billToAddressToUse", "billToAttention", "billToCity", "billToCountryID", "billToState", "billToZipCode", "city", "classification", "companyCategoryID", "companyName", "companyNumber", "companyType", "competitorID", "countryID", "createDate", "createdByResourceID", "currencyID", "fax", "impersonatorCreatorResourceID", "invoiceEmailMessageID", "invoiceMethod", "invoiceNonContractItemsToParentCompany", "invoiceTemplateID", "isActive", "isClientPortalActive", "isEnabledForComanaged", "isSample", "isTaskFireActive", "isTaxExempt", "lastActivityDate", "lastTrackedModifiedDateTime", "marketSegmentID", "ownerResourceID", "parentCompanyID", "phone", "postalCode", "purchaseOrderTemplateID", "quoteEmailMessageID", "quoteTemplateID", "sicCode", "state", "stockMarket", "stockSymbol", "surveyCompanyRating", "taxID", "taxRegionID", "territoryID", "webAddress", "userDefinedFields"],
|
|
17566
17848
|
"company_alerts": ["id", "alertText", "alertTypeID", "companyID", "soapParentPropertyId"],
|
|
@@ -17743,12 +18025,6 @@ var MODEL_DATE_FIELDS = {
|
|
|
17743
18025
|
"time_off_requests": ["approvedDateTime", "createDateTime", "endTime", "lastModifiedDateTime", "requestDate", "startTime"]
|
|
17744
18026
|
};
|
|
17745
18027
|
|
|
17746
|
-
// src/resources/base/types.ts
|
|
17747
|
-
var BOUNDED_QUERY_DEFAULT_LIMIT = 500;
|
|
17748
|
-
var BOUNDED_QUERY_MAX_LIMIT = 2500;
|
|
17749
|
-
var LATEST_MAX_COUNT_PROBES = 64;
|
|
17750
|
-
var MARKER_RECOVERY_LIMIT = 5;
|
|
17751
|
-
|
|
17752
18028
|
// src/resources/special/_shared/bounded-part.ts
|
|
17753
18029
|
async function boundedPart(http, path, conditions, limit, operation, includeFields) {
|
|
17754
18030
|
const body = { MaxRecords: limit, filter: conditions };
|
|
@@ -17781,6 +18057,10 @@ function relationsFor(resourceKey) {
|
|
|
17781
18057
|
const fields = MODEL_FIELD_SETS[resourceKey] ?? [];
|
|
17782
18058
|
return Object.keys(RELATION_TARGETS).filter((rel) => fields.includes(`${rel}ID`));
|
|
17783
18059
|
}
|
|
18060
|
+
function selfPrefixes(resourceKey) {
|
|
18061
|
+
const rels = Object.keys(RELATION_TARGETS).filter((rel) => RELATION_TARGETS[rel].resource === resourceKey);
|
|
18062
|
+
return [resourceKey, ...rels];
|
|
18063
|
+
}
|
|
17784
18064
|
var isRelationLeaf = (c) => isRecord2(c) && c.op !== "and" && c.op !== "or" && typeof c.field === "string" && c.field.includes(".");
|
|
17785
18065
|
function hasRelationConditions(conditions) {
|
|
17786
18066
|
if (conditions === void 0) return false;
|
|
@@ -17800,6 +18080,10 @@ function validateRelationLeaves(resourceKey, conditions, op) {
|
|
|
17800
18080
|
suggestedAction: valid.length > 0 ? `Use ${valid.map((r) => `${r}.<${RELATION_TARGETS[r].resource} field>`).join(" or ")} (one hop; field names from operations.fieldCatalog).` : `${resourceKey} has no relation filters; filter on its own fields.`
|
|
17801
18081
|
});
|
|
17802
18082
|
};
|
|
18083
|
+
if (selfPrefixes(resourceKey).includes(rel)) {
|
|
18084
|
+
if (rest.length > 0 || !sub) fail("is not <resource>.<field> (multi-hop relations are not supported)");
|
|
18085
|
+
continue;
|
|
18086
|
+
}
|
|
17803
18087
|
if (c.udf === true) fail("cannot be a UDF condition");
|
|
17804
18088
|
if (rest.length > 0 || !sub) fail("is not <relation>.<field> (multi-hop relations are not supported)");
|
|
17805
18089
|
if (!valid.includes(rel)) fail(`names an unknown relation for ${resourceKey} (valid: ${valid.join(", ") || "none"})`);
|
|
@@ -17833,7 +18117,12 @@ async function rewriteRelationFilters(http, resourceKey, conditions, op) {
|
|
|
17833
18117
|
const byRel = /* @__PURE__ */ new Map();
|
|
17834
18118
|
for (const c of list) {
|
|
17835
18119
|
if (isRelationLeaf(c)) {
|
|
17836
|
-
const
|
|
18120
|
+
const parts = c.field.split(".");
|
|
18121
|
+
if (parts.length === 2 && selfPrefixes(resourceKey).includes(parts[0])) {
|
|
18122
|
+
out.push({ ...c, field: parts[1] });
|
|
18123
|
+
continue;
|
|
18124
|
+
}
|
|
18125
|
+
const rel = parts[0];
|
|
17837
18126
|
if (mode === "and") {
|
|
17838
18127
|
byRel.set(rel, [...byRel.get(rel) ?? [], c]);
|
|
17839
18128
|
continue;
|
|
@@ -17851,20 +18140,128 @@ async function rewriteRelationFilters(http, resourceKey, conditions, op) {
|
|
|
17851
18140
|
out.push({ ...c, items });
|
|
17852
18141
|
continue;
|
|
17853
18142
|
}
|
|
17854
|
-
out.push(c);
|
|
18143
|
+
out.push(c);
|
|
18144
|
+
}
|
|
18145
|
+
for (const [rel, leaves] of byRel) {
|
|
18146
|
+
const ids2 = await parentIds(rel, leaves);
|
|
18147
|
+
if (ids2.length === 0) return NEVER;
|
|
18148
|
+
out.push({ field: `${rel}ID`, op: "in", value: ids2 });
|
|
18149
|
+
}
|
|
18150
|
+
return out.length === 0 && mode === "or" ? NEVER : out;
|
|
18151
|
+
};
|
|
18152
|
+
const rewritten = await rewrite(conds, "and");
|
|
18153
|
+
if (rewritten === NEVER) {
|
|
18154
|
+
return { never: true, note: `no parent record matched the relation conditions \u2014 0 ${resourceKey} rows (the ${resourceKey} query was not issued)` };
|
|
18155
|
+
}
|
|
18156
|
+
return { never: false, conditions: rewritten };
|
|
18157
|
+
}
|
|
18158
|
+
|
|
18159
|
+
// src/operations/rowLabels.ts
|
|
18160
|
+
var ROW_FK_TABLES = {
|
|
18161
|
+
companyID: "company",
|
|
18162
|
+
contractID: "contract",
|
|
18163
|
+
ticketID: "ticket",
|
|
18164
|
+
roleID: "role",
|
|
18165
|
+
billingCodeID: "billingCode"
|
|
18166
|
+
};
|
|
18167
|
+
var isResourceFkField = (field) => field === "resourceID" || field.endsWith("ResourceID");
|
|
18168
|
+
var LABEL_NAME_MAX_CHARS = 200;
|
|
18169
|
+
function untrustedName(name) {
|
|
18170
|
+
if (name === null) return null;
|
|
18171
|
+
const [[capped]] = truncateNoteChars([{ v: name }], LABEL_NAME_MAX_CHARS, ["v"]);
|
|
18172
|
+
return wrapUntrusted(capped.v) ?? capped.v;
|
|
18173
|
+
}
|
|
18174
|
+
function readNamesOpt(client, value, where) {
|
|
18175
|
+
if (value === void 0) return client?.config?.labels ?? false;
|
|
18176
|
+
if (typeof value !== "boolean") {
|
|
18177
|
+
throw new AutotaskConfigError(`${where}: names must be a boolean (got ${typeof value}) \u2014 true adds label siblings, false returns raw ids only, absent uses the client's labels option (off by default)`);
|
|
18178
|
+
}
|
|
18179
|
+
return value;
|
|
18180
|
+
}
|
|
18181
|
+
async function labelRows(client, resourceKey, rows) {
|
|
18182
|
+
const keys = /* @__PURE__ */ new Set();
|
|
18183
|
+
for (const row of rows) for (const k of Object.keys(row)) keys.add(k);
|
|
18184
|
+
if (keys.size === 0) return { rows: [...rows] };
|
|
18185
|
+
let picklistFields = [];
|
|
18186
|
+
const modelFields = MODEL_FIELD_SETS[resourceKey] ?? [];
|
|
18187
|
+
const candidates = [...keys].filter((f) => f !== "id" && modelFields.includes(f) && ROW_FK_TABLES[f] === void 0 && !isResourceFkField(f));
|
|
18188
|
+
if (candidates.length > 0) {
|
|
18189
|
+
try {
|
|
18190
|
+
const catalog = await fieldCatalog(client, { resources: [resourceKey], labels: true });
|
|
18191
|
+
const row = catalog.rows.find((r) => r.resource === resourceKey);
|
|
18192
|
+
if (row !== void 0 && row.error === null && row.fields !== void 0) {
|
|
18193
|
+
picklistFields = row.fields.filter((f) => f.isPickList).map((f) => f.name).filter((f) => candidates.includes(f));
|
|
18194
|
+
}
|
|
18195
|
+
} catch (err) {
|
|
18196
|
+
if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
|
|
17855
18197
|
}
|
|
17856
|
-
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
18198
|
+
}
|
|
18199
|
+
const labelled = (field) => picklistFields.includes(field) || ROW_FK_TABLES[field] !== void 0 || isResourceFkField(field);
|
|
18200
|
+
if (![...keys].some(labelled)) return { rows: [...rows] };
|
|
18201
|
+
const builder = createNamePlan(picklistFields, resourceKey);
|
|
18202
|
+
for (const row of rows) {
|
|
18203
|
+
for (const [field, value] of Object.entries(row)) {
|
|
18204
|
+
const table = ROW_FK_TABLES[field];
|
|
18205
|
+
if ((value === 0 || value === "0") && table !== "company") continue;
|
|
18206
|
+
if (table !== void 0) builder.addFk(table, value);
|
|
18207
|
+
else if (isResourceFkField(field)) builder.addResource(value);
|
|
17860
18208
|
}
|
|
17861
|
-
return out.length === 0 && mode === "or" ? NEVER : out;
|
|
17862
|
-
};
|
|
17863
|
-
const rewritten = await rewrite(conds, "and");
|
|
17864
|
-
if (rewritten === NEVER) {
|
|
17865
|
-
return { never: true, note: `no parent record matched the relation conditions \u2014 0 ${resourceKey} rows (the ${resourceKey} query was not issued)` };
|
|
17866
18209
|
}
|
|
17867
|
-
|
|
18210
|
+
const plan = builder.plan();
|
|
18211
|
+
const names = plan.resourceIds.length === 0 && plan.recordFks.length === 0 && picklistFields.length === 0 ? void 0 : await resolveCompositeNames(client.http, plan);
|
|
18212
|
+
const out = rows.map((row) => {
|
|
18213
|
+
const r = { ...row };
|
|
18214
|
+
for (const [field, value] of Object.entries(row)) {
|
|
18215
|
+
const has2 = value !== null && value !== void 0 && names !== void 0;
|
|
18216
|
+
if (picklistFields.includes(field)) r[`${field}Label`] = has2 ? names.labelFor(field, value) : null;
|
|
18217
|
+
const table = ROW_FK_TABLES[field];
|
|
18218
|
+
const set = has2 && (table === "company" || value !== 0 && value !== "0");
|
|
18219
|
+
if (table !== void 0) r[`${field}Name`] = set && isVendorId(value) ? untrustedName(names.recordName(table, value)) : null;
|
|
18220
|
+
else if (isResourceFkField(field)) r[`${field}Name`] = set && isVendorId(value) ? untrustedName(names.resourceName(value)) : null;
|
|
18221
|
+
}
|
|
18222
|
+
return r;
|
|
18223
|
+
});
|
|
18224
|
+
const capHit = names?.section.truncated === true;
|
|
18225
|
+
const unresolved = Object.keys(names?.section.reasons ?? {});
|
|
18226
|
+
if (capHit === false && unresolved.length === 0) return { rows: out };
|
|
18227
|
+
const parts = [];
|
|
18228
|
+
if (capHit) parts.push(`ids left unlabelled (their *Name is null): the per-call cap of ${DEFAULT_MAX_NAME_READS} uncached name reads was hit \u2014 names already read are cached, so repeating or narrowing the call labels more`);
|
|
18229
|
+
if (unresolved.length > 0) parts.push(`picklist label(s) unavailable for ${unresolved.join(", ")} (see reasons)`);
|
|
18230
|
+
return { rows: out, names: { note: parts.join("; "), ...Object.keys(names.section.reasons).length > 0 ? { reasons: names.section.reasons } : {} } };
|
|
18231
|
+
}
|
|
18232
|
+
async function labelFetchResults(client, result) {
|
|
18233
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
18234
|
+
result.results.forEach((r, i) => {
|
|
18235
|
+
if (r.found && r.value !== null) byResource.set(r.resource, [...byResource.get(r.resource) ?? [], i]);
|
|
18236
|
+
});
|
|
18237
|
+
const results = [...result.results];
|
|
18238
|
+
const notes = [];
|
|
18239
|
+
const reasons = {};
|
|
18240
|
+
for (const [resource, indices] of byResource) {
|
|
18241
|
+
const labelled = await labelRows(client, resource, indices.map((i) => results[i].value));
|
|
18242
|
+
indices.forEach((idx, j) => {
|
|
18243
|
+
results[idx] = { ...results[idx], value: labelled.rows[j] };
|
|
18244
|
+
});
|
|
18245
|
+
if (labelled.names !== void 0) {
|
|
18246
|
+
notes.push(labelled.names.note);
|
|
18247
|
+
for (const [field, why] of Object.entries(labelled.names.reasons ?? {})) reasons[field] = why;
|
|
18248
|
+
}
|
|
18249
|
+
}
|
|
18250
|
+
if (notes.length === 0) return { ...result, results };
|
|
18251
|
+
return { ...result, results, names: { note: notes.join("; "), ...Object.keys(reasons).length > 0 ? { reasons } : {} } };
|
|
18252
|
+
}
|
|
18253
|
+
|
|
18254
|
+
// src/operations/walkBag.ts
|
|
18255
|
+
function validateWalkBag(walk, prefix, details) {
|
|
18256
|
+
if (walk === void 0) return void 0;
|
|
18257
|
+
const wr = typeof walk === "object" && walk !== null && !Array.isArray(walk) ? walk.maxRows : void 0;
|
|
18258
|
+
if (!Number.isInteger(wr) || wr < 1) {
|
|
18259
|
+
throw new AutotaskConfigError(
|
|
18260
|
+
`${prefix} walk.maxRows must be a positive integer (got ${String(wr)}) \u2014 it is the walk's TOTAL row cap (the bounding cap = min(limit, walk.maxRows))`,
|
|
18261
|
+
details
|
|
18262
|
+
);
|
|
18263
|
+
}
|
|
18264
|
+
return wr;
|
|
17868
18265
|
}
|
|
17869
18266
|
|
|
17870
18267
|
// src/resources/base/query.ts
|
|
@@ -17968,19 +18365,19 @@ var QueryBase = class {
|
|
|
17968
18365
|
/**
|
|
17969
18366
|
* Stream items across pages (the resource `query` surface — the MCP
|
|
17970
18367
|
* server's `output_path` walk). Issue #281: relation filters resolve first
|
|
17971
|
-
* (as queryBounded)
|
|
17972
|
-
*
|
|
17973
|
-
*
|
|
17974
|
-
*
|
|
17975
|
-
*
|
|
17976
|
-
* raw rows.
|
|
18368
|
+
* (as queryBounded). Label siblings are opt-in (issue #287): the per-call
|
|
18369
|
+
* `names` key is the explicit override, absent = the client's `labels`
|
|
18370
|
+
* option (off by default). When on, labelling runs per page, so the cold
|
|
18371
|
+
* name-read cap (20) applies per page; a capped id gets a null `*Name`
|
|
18372
|
+
* (an iterator has no result envelope for the `names` summary). A
|
|
18373
|
+
* resource built without a client (bare `HttpClient`) yields raw rows.
|
|
17977
18374
|
*/
|
|
17978
18375
|
async *queryItems(params) {
|
|
17979
18376
|
const { names, ...rest } = params ?? {};
|
|
17980
|
-
const
|
|
18377
|
+
const client = this.http.client;
|
|
18378
|
+
const label = readNamesOpt(client, names, `${this.config.key}.query`);
|
|
17981
18379
|
const walk = await this.relationWalkParams(rest, `${this.config.key}.query`);
|
|
17982
18380
|
if (walk === void 0) return;
|
|
17983
|
-
const client = this.http.client;
|
|
17984
18381
|
if (!label || client === void 0) {
|
|
17985
18382
|
yield* paginateItems(this.pageFetcher(walk), { pageSize: this.walkPageSize(walk) });
|
|
17986
18383
|
return;
|
|
@@ -18049,8 +18446,7 @@ var QueryBase = class {
|
|
|
18049
18446
|
`queryBounded: noteChars must be an integer >= 1 (got ${String(noteChars)}) \u2014 it is the per-field char cap on the untrusted-class free-text fields (absent = the raw, untruncated result)`
|
|
18050
18447
|
);
|
|
18051
18448
|
}
|
|
18052
|
-
|
|
18053
|
-
const latest = walkParams.latest;
|
|
18449
|
+
const latest = params?.latest;
|
|
18054
18450
|
if (latest !== void 0) {
|
|
18055
18451
|
if (!Number.isInteger(latest) || latest < 1 || latest > BOUNDED_QUERY_MAX_LIMIT) {
|
|
18056
18452
|
throw new AutotaskConfigError(
|
|
@@ -18063,15 +18459,27 @@ var QueryBase = class {
|
|
|
18063
18459
|
);
|
|
18064
18460
|
}
|
|
18065
18461
|
}
|
|
18462
|
+
const walk = params?.walk;
|
|
18463
|
+
const maxRows = validateWalkBag(walk, "queryBounded:");
|
|
18464
|
+
if (walk !== void 0 && latest !== void 0) {
|
|
18465
|
+
throw new AutotaskConfigError(
|
|
18466
|
+
"queryBounded: walk.maxRows is not combinable with latest \u2014 the count-anchored mechanism has its own walk window"
|
|
18467
|
+
);
|
|
18468
|
+
}
|
|
18469
|
+
const cap = maxRows !== void 0 ? Math.min(limit, maxRows) : limit;
|
|
18470
|
+
let walkParams = params ?? {};
|
|
18066
18471
|
const relation = await rewriteRelationFilters(this.http, this.config.key, normalizedConditions(walkParams.filter), `${this.config.key}.query`);
|
|
18067
18472
|
if (relation?.never === true) return { items: [], fetched: 0, hasMore: false, pagesWalked: 0, relationNote: relation.note };
|
|
18068
18473
|
if (relation !== void 0) walkParams = { ...walkParams, filter: relation.conditions };
|
|
18069
18474
|
if (latest !== void 0) {
|
|
18070
18475
|
return this.applyNoteChars(await this.latestWalk(walkParams, Math.max(limit, latest), latest), noteChars);
|
|
18071
18476
|
}
|
|
18072
|
-
const { items, pages, guard, sliced, cursor } = await this.walkBounded(walkParams, limit);
|
|
18073
|
-
const hasMore = (items.length >=
|
|
18477
|
+
const { items, pages, guard, sliced, cursor } = await this.walkBounded(walkParams, cap, limit);
|
|
18478
|
+
const hasMore = (items.length >= cap || guard) && (sliced > 0 || cursor !== null);
|
|
18074
18479
|
const result = { items, fetched: items.length, hasMore, pagesWalked: pages };
|
|
18480
|
+
if (maxRows !== void 0) {
|
|
18481
|
+
result.truncated = items.length >= maxRows && (sliced > 0 || cursor !== null || guard);
|
|
18482
|
+
}
|
|
18075
18483
|
this.stampEmptyQueryResult(result, params);
|
|
18076
18484
|
return this.applyNoteChars(result, noteChars);
|
|
18077
18485
|
}
|
|
@@ -18102,13 +18510,13 @@ var QueryBase = class {
|
|
|
18102
18510
|
* SOFT stop (never a throw). ONE per-walk closure (walks never share
|
|
18103
18511
|
* state).
|
|
18104
18512
|
* @internal */
|
|
18105
|
-
async walkBounded(p, limit) {
|
|
18513
|
+
async walkBounded(p, limit, pageSizeBase) {
|
|
18106
18514
|
const state = { cursor: null };
|
|
18107
18515
|
const items = [];
|
|
18108
18516
|
let pages = 0;
|
|
18109
18517
|
let sliced = 0;
|
|
18110
18518
|
let guard = false;
|
|
18111
|
-
const pageSize = Math.min(MAX_PAGE_SIZE, limit);
|
|
18519
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, pageSizeBase ?? limit);
|
|
18112
18520
|
for (; ; ) {
|
|
18113
18521
|
if (pages >= MAX_PAGES) {
|
|
18114
18522
|
guard = true;
|
|
@@ -18724,1333 +19132,884 @@ var MetaBase = class extends QueryBase {
|
|
|
18724
19132
|
* `searchFields`, capped by the client caps (`maxScanRecords` /
|
|
18725
19133
|
* `maxScanPages`). A hit is cost `client-scan`; a completed scan with no
|
|
18726
19134
|
* match → null; a cap-tripped scan THROWS RESOLUTION_TRUNCATED — never a
|
|
18727
|
-
* silent null.
|
|
18728
|
-
* - bare value on an id-only resource → AutotaskConfigError (CONFIG_ERROR).
|
|
18729
|
-
*/
|
|
18730
|
-
async resolveDetailed(identifier) {
|
|
18731
|
-
let numeric = null;
|
|
18732
|
-
if (isIdentifierCandidate(identifier)) {
|
|
18733
|
-
numeric = parseIdentifier(identifier, "id");
|
|
18734
|
-
}
|
|
18735
|
-
if (numeric !== null) {
|
|
18736
|
-
const record = await this.fetchRecordRaw(this.resourcePath, numeric);
|
|
18737
|
-
return { value: record, resolutionCost: "direct", scanned: 0, scanTruncated: false };
|
|
18738
|
-
}
|
|
18739
|
-
if (typeof identifier !== "string" || identifier.trim() === "") {
|
|
18740
|
-
throw new AutotaskConfigError("resolve: identifier must be a numeric id or a non-empty value");
|
|
18741
|
-
}
|
|
18742
|
-
const value = identifier.trim();
|
|
18743
|
-
const fields = this.config.searchFields ?? [];
|
|
18744
|
-
if (fields.length === 0) {
|
|
18745
|
-
throw new AutotaskConfigError(
|
|
18746
|
-
`resolve: ${this.config.key} has no primary text field (ids only)`
|
|
18747
|
-
);
|
|
18748
|
-
}
|
|
18749
|
-
const leaf = fields.map((field) => ({ field, op: "eq", value }));
|
|
18750
|
-
const root = leaf.length === 1 ? leaf[0] : { op: "or", items: leaf };
|
|
18751
|
-
const items = await this.queryPage1([root], 2);
|
|
18752
|
-
if (items.length === 1) {
|
|
18753
|
-
return { value: items[0], resolutionCost: "server-filter", scanned: 0, scanTruncated: false };
|
|
18754
|
-
}
|
|
18755
|
-
if (items.length >= 2) {
|
|
18756
|
-
const ids2 = items.map((m) => m.id).filter((id) => typeof id === "number");
|
|
18757
|
-
throw new ResolutionError(
|
|
18758
|
-
`resolve: "${value}" is ambiguous on ${this.config.key} (matched ${items.length} records)`,
|
|
18759
|
-
{
|
|
18760
|
-
code: "RESOLUTION_AMBIGUOUS",
|
|
18761
|
-
resourceIds: ids2,
|
|
18762
|
-
details: { idRefs: ids2.map((cid) => ({ entity: this.entityName, id: cid })) },
|
|
18763
|
-
// issue #59 PR-B: the miss-channel pointer (the generic floor names
|
|
18764
|
-
// the resource's own search op — reachable only where searchFields
|
|
18765
|
-
// is non-empty, so the named op always exists here).
|
|
18766
|
-
suggestedAction: `Narrow the identifier to the full exact value, or run ${this.config.key}.search (contains match) for candidate records to disambiguate.`
|
|
18767
|
-
}
|
|
18768
|
-
);
|
|
18769
|
-
}
|
|
18770
|
-
if (!this.resolutionCaps.scanOnMiss) {
|
|
18771
|
-
return { value: null, resolutionCost: "server-filter", scanned: 0, scanTruncated: false, triedFields: fields };
|
|
18772
|
-
}
|
|
18773
|
-
const scan = await this.scanForExactMatch(value, fields);
|
|
18774
|
-
if (scan.found !== null) {
|
|
18775
|
-
return {
|
|
18776
|
-
value: scan.found,
|
|
18777
|
-
resolutionCost: "client-scan",
|
|
18778
|
-
scanned: scan.scanned,
|
|
18779
|
-
scanTruncated: false
|
|
18780
|
-
};
|
|
18781
|
-
}
|
|
18782
|
-
if (scan.truncated) {
|
|
18783
|
-
throw new ResolutionError(
|
|
18784
|
-
`resolve: "${value}" is unverified on ${this.config.key} \u2014 the bounded client scan read ${scan.scanned} records (cap) without completing; a complete non-match cannot be proven`,
|
|
18785
|
-
{
|
|
18786
|
-
code: "RESOLUTION_TRUNCATED",
|
|
18787
|
-
// issue #59 PR-B: the pointer channel on the unverified-miss outcome
|
|
18788
|
-
// (the contains-candidates channel is the resource's search op).
|
|
18789
|
-
suggestedAction: `Retry with a more specific identifier, or narrow the search to fewer records (contains candidates: ${this.config.key}.search).`
|
|
18790
|
-
}
|
|
18791
|
-
);
|
|
18792
|
-
}
|
|
18793
|
-
return { value: null, resolutionCost: "client-scan", scanned: scan.scanned, scanTruncated: false, triedFields: fields };
|
|
18794
|
-
}
|
|
18795
|
-
/**
|
|
18796
|
-
* Issue #78: `resolve` with the miss explained. A hit is `{ record, resolutionCost }`
|
|
18797
|
-
* (unchanged). A miss is never a bare `null`: `miss` carries the fields and operator
|
|
18798
|
-
* tried, the blind-surface stamp when the surface is blind, and the next steps.
|
|
18799
|
-
* Thrown outcomes (not found by id, ambiguous, truncated) are unchanged.
|
|
18800
|
-
*/
|
|
18801
|
-
async resolveWithDetail(identifier) {
|
|
18802
|
-
const r = await this.resolveDetailed(identifier);
|
|
18803
|
-
if (r.value !== null) return { record: r.value, resolutionCost: r.resolutionCost };
|
|
18804
|
-
return { record: null, resolutionCost: r.resolutionCost, miss: this.missDetail("eq", r.triedFields ?? this.config.searchFields ?? []) };
|
|
18805
|
-
}
|
|
18806
|
-
/**
|
|
18807
|
-
* Issue #78: `search` with an empty result explained (a bare `[]` reads as complete on a
|
|
18808
|
-
* blind surface). Hits are `{ items }` (unchanged); an empty result adds `miss`.
|
|
18809
|
-
*/
|
|
18810
|
-
async searchWithDetail(query, opts = {}) {
|
|
18811
|
-
const items = await this.searchBase(query, opts);
|
|
18812
|
-
if (items.length > 0) return { items };
|
|
18813
|
-
return { items, miss: this.missDetail("contains", this.config.searchFields ?? []) };
|
|
18814
|
-
}
|
|
18815
|
-
/** The shared miss explanation for `resolveWithDetail` / `searchWithDetail`. @internal */
|
|
18816
|
-
missDetail(operator, fields) {
|
|
18817
|
-
const key = this.config.key;
|
|
18818
|
-
const probe = fields.map((field) => ({ field, op: operator, value: "" }));
|
|
18819
|
-
const blind = blindStampFor(BLIND_SURFACE_STAMPS[key]?.query, probe);
|
|
18820
|
-
const next = [];
|
|
18821
|
-
if (blind !== void 0) {
|
|
18822
|
-
next.push(`This surface is blind to records the API user does not own, so a miss is not proof of absence: ${blind.authority} returns the record when the id is known.`);
|
|
18823
|
-
}
|
|
18824
|
-
next.push(
|
|
18825
|
-
operator === "eq" ? `Matching is exact on ${fields.join(", ")} (other identifier forms were not tried); ${key}.search finds contains candidates.` : `Matching is a contains match on ${fields.join(", ")}; try a shorter or different fragment, or ${key}.resolve with the full exact value.`
|
|
18826
|
-
);
|
|
18827
|
-
return { tried: { fields: [...fields], operator }, ...blind !== void 0 ? { blindSurface: blind } : {}, next };
|
|
18828
|
-
}
|
|
18829
|
-
/** The plain value projection of `resolveDetailed` (the `resolve` floor). */
|
|
18830
|
-
async resolveBase(identifier) {
|
|
18831
|
-
return (await this.resolveDetailed(identifier)).value;
|
|
18832
|
-
}
|
|
18833
|
-
/** The items of one page of the resource's own `/query`. */
|
|
18834
|
-
async queryPage1(conditions, maxRecords) {
|
|
18835
|
-
const body = {
|
|
18836
|
-
filter: buildFilter(conditions).filter,
|
|
18837
|
-
MaxRecords: maxRecords
|
|
18838
|
-
};
|
|
18839
|
-
const res = await this.http.request({
|
|
18840
|
-
method: "POST",
|
|
18841
|
-
idempotent: true,
|
|
18842
|
-
path: `${this.resourcePath}/query`,
|
|
18843
|
-
body,
|
|
18844
|
-
operation: `${this.config.key}.resolve`
|
|
18845
|
-
});
|
|
18846
|
-
return unwrapItems(res, `${this.config.key}.resolve`).items;
|
|
18847
|
-
}
|
|
18848
|
-
/**
|
|
18849
|
-
* The bounded client-scan fallback (section 19.2): an unfiltered walk of
|
|
18850
|
-
* the resource's own query endpoint, exact match on `fields`, bounded by
|
|
18851
|
-
* BOTH client caps (records AND pages). `truncated` is true when the caps
|
|
18852
|
-
* were hit before the walk completed (a complete non-match is unproven).
|
|
18853
|
-
*/
|
|
18854
|
-
async scanForExactMatch(value, fields) {
|
|
18855
|
-
const caps = this.resolutionCaps;
|
|
18856
|
-
const pageSize = Math.max(1, Math.floor(caps.maxScanRecords / caps.maxScanPages));
|
|
18857
|
-
const state = { cursor: null };
|
|
18858
|
-
let scanned = 0;
|
|
18859
|
-
let pages = 0;
|
|
18860
|
-
let found = null;
|
|
18861
|
-
let exhausted = false;
|
|
18862
|
-
for (; ; ) {
|
|
18863
|
-
const page = await this.fetchPage({ maxRecords: pageSize }, pages + 1, pageSize, state);
|
|
18864
|
-
pages += 1;
|
|
18865
|
-
for (const item of page.items) {
|
|
18866
|
-
scanned += 1;
|
|
18867
|
-
const rec = item;
|
|
18868
|
-
if (fields.some((field) => rec[field] === value)) {
|
|
18869
|
-
found = item;
|
|
18870
|
-
break;
|
|
18871
|
-
}
|
|
18872
|
-
}
|
|
18873
|
-
if (found !== null) break;
|
|
18874
|
-
if (!page.hasMore) {
|
|
18875
|
-
exhausted = true;
|
|
18876
|
-
break;
|
|
18877
|
-
}
|
|
18878
|
-
if (pages >= caps.maxScanPages || scanned >= caps.maxScanRecords) break;
|
|
18879
|
-
}
|
|
18880
|
-
return { found, scanned, truncated: found === null && !exhausted };
|
|
18881
|
-
}
|
|
18882
|
-
/**
|
|
18883
|
-
* `findBy<field>` helpers: exact (`eq`) filter on one vendor field, bounded
|
|
18884
|
-
* to 2 records — 0 → null, 1 → the record, ≥2 → ResolutionError with the
|
|
18885
|
-
* candidate ids.
|
|
18886
|
-
*/
|
|
18887
|
-
async findByField(field, value) {
|
|
18888
|
-
if (value.trim() === "") throw new AutotaskConfigError(`findBy${field}: empty value`);
|
|
18889
|
-
const matches = await this.queryEntity(
|
|
18890
|
-
`${this.resourcePath}/query`,
|
|
18891
|
-
[{ field, op: "eq", value: value.trim() }],
|
|
18892
|
-
{ maxRecords: 2 }
|
|
18893
|
-
);
|
|
18894
|
-
if (matches.length === 0) return null;
|
|
18895
|
-
if (matches.length > 1) {
|
|
18896
|
-
const ids2 = matches.map((m) => m.id).filter((id) => typeof id === "number");
|
|
18897
|
-
throw new ResolutionError(
|
|
18898
|
-
`findBy${field}: "${value}" is ambiguous on ${this.config.key} (matched ${matches.length} records)`,
|
|
18899
|
-
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids2, details: { idRefs: ids2.map((cid) => ({ entity: this.entityName, id: cid })) } }
|
|
18900
|
-
);
|
|
18901
|
-
}
|
|
18902
|
-
return matches[0];
|
|
18903
|
-
}
|
|
18904
|
-
/**
|
|
18905
|
-
* `search` — the bounded contains surface (no vendor free-text search):
|
|
18906
|
-
* `contains` on every `searchFields` field (OR-grouped), `MaxRecords` =
|
|
18907
|
-
* `limit` (default 25, clamped to 1..500). Empty query → AutotaskConfigError.
|
|
19135
|
+
* silent null.
|
|
19136
|
+
* - bare value on an id-only resource → AutotaskConfigError (CONFIG_ERROR).
|
|
18908
19137
|
*/
|
|
18909
|
-
async
|
|
18910
|
-
|
|
18911
|
-
if (
|
|
19138
|
+
async resolveDetailed(identifier) {
|
|
19139
|
+
let numeric = null;
|
|
19140
|
+
if (isIdentifierCandidate(identifier)) {
|
|
19141
|
+
numeric = parseIdentifier(identifier, "id");
|
|
19142
|
+
}
|
|
19143
|
+
if (numeric !== null) {
|
|
19144
|
+
const record = await this.fetchRecordRaw(this.resourcePath, numeric);
|
|
19145
|
+
return { value: record, resolutionCost: "direct", scanned: 0, scanTruncated: false };
|
|
19146
|
+
}
|
|
19147
|
+
if (typeof identifier !== "string" || identifier.trim() === "") {
|
|
19148
|
+
throw new AutotaskConfigError("resolve: identifier must be a numeric id or a non-empty value");
|
|
19149
|
+
}
|
|
19150
|
+
const value = identifier.trim();
|
|
18912
19151
|
const fields = this.config.searchFields ?? [];
|
|
18913
19152
|
if (fields.length === 0) {
|
|
18914
|
-
throw new AutotaskConfigError(
|
|
19153
|
+
throw new AutotaskConfigError(
|
|
19154
|
+
`resolve: ${this.config.key} has no primary text field (ids only)`
|
|
19155
|
+
);
|
|
18915
19156
|
}
|
|
18916
|
-
const
|
|
18917
|
-
const leaf = fields.map((field) => ({ field, op: "contains", value: text }));
|
|
19157
|
+
const leaf = fields.map((field) => ({ field, op: "eq", value }));
|
|
18918
19158
|
const root = leaf.length === 1 ? leaf[0] : { op: "or", items: leaf };
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
18922
|
-
}
|
|
19159
|
+
const items = await this.queryPage1([root], 2);
|
|
19160
|
+
if (items.length === 1) {
|
|
19161
|
+
return { value: items[0], resolutionCost: "server-filter", scanned: 0, scanTruncated: false };
|
|
18923
19162
|
}
|
|
18924
|
-
if (
|
|
18925
|
-
|
|
19163
|
+
if (items.length >= 2) {
|
|
19164
|
+
const ids2 = items.map((m) => m.id).filter((id) => typeof id === "number");
|
|
19165
|
+
throw new ResolutionError(
|
|
19166
|
+
`resolve: "${value}" is ambiguous on ${this.config.key} (matched ${items.length} records)`,
|
|
19167
|
+
{
|
|
19168
|
+
code: "RESOLUTION_AMBIGUOUS",
|
|
19169
|
+
resourceIds: ids2,
|
|
19170
|
+
details: { idRefs: ids2.map((cid) => ({ entity: this.entityName, id: cid })) },
|
|
19171
|
+
// issue #59 PR-B: the miss-channel pointer (the generic floor names
|
|
19172
|
+
// the resource's own search op — reachable only where searchFields
|
|
19173
|
+
// is non-empty, so the named op always exists here).
|
|
19174
|
+
suggestedAction: `Narrow the identifier to the full exact value, or run ${this.config.key}.search (contains match) for candidate records to disambiguate.`
|
|
19175
|
+
}
|
|
19176
|
+
);
|
|
18926
19177
|
}
|
|
18927
|
-
if (
|
|
18928
|
-
|
|
19178
|
+
if (!this.resolutionCaps.scanOnMiss) {
|
|
19179
|
+
return { value: null, resolutionCost: "server-filter", scanned: 0, scanTruncated: false, triedFields: fields };
|
|
18929
19180
|
}
|
|
18930
|
-
const
|
|
18931
|
-
|
|
18932
|
-
|
|
18933
|
-
|
|
18934
|
-
|
|
18935
|
-
|
|
18936
|
-
|
|
18937
|
-
|
|
18938
|
-
return rows.map((row) => {
|
|
18939
|
-
const r = row;
|
|
18940
|
-
if (!Array.isArray(r.userDefinedFields) || r.userDefinedFields.length > 0) return row;
|
|
18941
|
-
const rest = { ...r };
|
|
18942
|
-
delete rest.userDefinedFields;
|
|
18943
|
-
return rest;
|
|
18944
|
-
});
|
|
18945
|
-
}
|
|
18946
|
-
};
|
|
18947
|
-
function isEmpty(value) {
|
|
18948
|
-
if (value === null || value === void 0) return true;
|
|
18949
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
18950
|
-
return typeof value === "object" && Object.keys(value).length === 0;
|
|
18951
|
-
}
|
|
18952
|
-
|
|
18953
|
-
// src/resources/base/mutations.ts
|
|
18954
|
-
var MutationBase = class extends MetaBase {
|
|
18955
|
-
// ------------------------------------------------------------------
|
|
18956
|
-
// Mutations
|
|
18957
|
-
// ------------------------------------------------------------------
|
|
18958
|
-
/** Normalise a mutation body: `id` OR `itemId` → `id`; UDF map → UDF array (UDF-capable only). */
|
|
18959
|
-
normalizeMutationBody(data) {
|
|
18960
|
-
const body = { ...data };
|
|
18961
|
-
if (body.itemId !== void 0 && body.id === void 0) {
|
|
18962
|
-
body.id = body.itemId;
|
|
18963
|
-
delete body.itemId;
|
|
19181
|
+
const scan = await this.scanForExactMatch(value, fields);
|
|
19182
|
+
if (scan.found !== null) {
|
|
19183
|
+
return {
|
|
19184
|
+
value: scan.found,
|
|
19185
|
+
resolutionCost: "client-scan",
|
|
19186
|
+
scanned: scan.scanned,
|
|
19187
|
+
scanTruncated: false
|
|
19188
|
+
};
|
|
18964
19189
|
}
|
|
18965
|
-
if (
|
|
18966
|
-
|
|
19190
|
+
if (scan.truncated) {
|
|
19191
|
+
throw new ResolutionError(
|
|
19192
|
+
`resolve: "${value}" is unverified on ${this.config.key} \u2014 the bounded client scan read ${scan.scanned} records (cap) without completing; a complete non-match cannot be proven`,
|
|
19193
|
+
{
|
|
19194
|
+
code: "RESOLUTION_TRUNCATED",
|
|
19195
|
+
// issue #59 PR-B: the pointer channel on the unverified-miss outcome
|
|
19196
|
+
// (the contains-candidates channel is the resource's search op).
|
|
19197
|
+
suggestedAction: `Retry with a more specific identifier, or narrow the search to fewer records (contains candidates: ${this.config.key}.search).`
|
|
19198
|
+
}
|
|
19199
|
+
);
|
|
18967
19200
|
}
|
|
18968
|
-
return
|
|
18969
|
-
}
|
|
18970
|
-
/** `POST /V1.0/<E>` → `{ id }` (issue #13: a drifted 2xx runs the bounded marker recovery where the plan pins a marker field). */
|
|
18971
|
-
async createGlobal(data, opts = {}) {
|
|
18972
|
-
const body = this.normalizeMutationBody(data);
|
|
18973
|
-
const res = await this.http.request({
|
|
18974
|
-
method: "POST",
|
|
18975
|
-
path: this.resourcePath,
|
|
18976
|
-
body,
|
|
18977
|
-
operation: `${this.config.key}.create`,
|
|
18978
|
-
maxRetries: opts.maxRetries
|
|
18979
|
-
});
|
|
18980
|
-
return this.unwrapCreateOperation(res, body, opts);
|
|
19201
|
+
return { value: null, resolutionCost: "client-scan", scanned: scan.scanned, scanTruncated: false, triedFields: fields };
|
|
18981
19202
|
}
|
|
18982
19203
|
/**
|
|
18983
|
-
* Issue #
|
|
18984
|
-
*
|
|
18985
|
-
*
|
|
18986
|
-
*
|
|
18987
|
-
* not the specced envelope / `{ id }` / `{ itemId }` alternative) with a
|
|
18988
|
-
* plan-pinned marker field AND `recoverIdFromMarker !== false` runs the
|
|
18989
|
-
* fallback — and it can only run on the APPLY path: a dry run returns the
|
|
18990
|
-
* pinned envelope BEFORE the mutating request, so the fallback never sees
|
|
18991
|
-
* a dry-run response (skipped entirely by construction).
|
|
18992
|
-
* @internal
|
|
19204
|
+
* Issue #78: `resolve` with the miss explained. A hit is `{ record, resolutionCost }`
|
|
19205
|
+
* (unchanged). A miss is never a bare `null`: `miss` carries the fields and operator
|
|
19206
|
+
* tried, the blind-surface stamp when the surface is blind, and the next steps.
|
|
19207
|
+
* Thrown outcomes (not found by id, ambiguous, truncated) are unchanged.
|
|
18993
19208
|
*/
|
|
18994
|
-
async
|
|
18995
|
-
const
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
} catch (err) {
|
|
18999
|
-
if (!isAutotaskError(err) || err.code !== "INVALID_RESPONSE") throw err;
|
|
19000
|
-
const pin = RESPONSE_DRIFT_MARKERS[this.config.key];
|
|
19001
|
-
if (pin === void 0 || opts.recoverIdFromMarker === false) throw err;
|
|
19002
|
-
return this.recoverCreatedIdByMarker(err, body, wireBody, opts, pin);
|
|
19003
|
-
}
|
|
19209
|
+
async resolveWithDetail(identifier) {
|
|
19210
|
+
const r = await this.resolveDetailed(identifier);
|
|
19211
|
+
if (r.value !== null) return { record: r.value, resolutionCost: r.resolutionCost };
|
|
19212
|
+
return { record: null, resolutionCost: r.resolutionCost, miss: this.missDetail("eq", r.triedFields ?? this.config.searchFields ?? []) };
|
|
19004
19213
|
}
|
|
19005
19214
|
/**
|
|
19006
|
-
* Issue #
|
|
19007
|
-
* surface
|
|
19008
|
-
* zero mutating wire; the fallback never issues anything else). Outcomes:
|
|
19009
|
-
* exactly ONE numeric match → the normal success result stamped
|
|
19010
|
-
* `idFrom: 'marker-recovery'` + the `shapeDrift` note; zero matches, more
|
|
19011
|
-
* than one, or a cap-tripped search (a full 5 + a next cursor) → the
|
|
19012
|
-
* existing `INVALID_RESPONSE` with the drift detail + the failed search
|
|
19013
|
-
* (candidate ids listed — never a guessed id).
|
|
19014
|
-
* @internal
|
|
19215
|
+
* Issue #78: `search` with an empty result explained (a bare `[]` reads as complete on a
|
|
19216
|
+
* blind surface). Hits are `{ items }` (unchanged); an empty result adds `miss`.
|
|
19015
19217
|
*/
|
|
19016
|
-
async
|
|
19017
|
-
const
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
|
|
19024
|
-
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
const { items, pageDetails } = unwrapItems(res, operation);
|
|
19030
|
-
const truncated = items.length >= MARKER_RECOVERY_LIMIT && pageDetails.nextPageUrl != null;
|
|
19031
|
-
const drift = {
|
|
19032
|
-
observedShape,
|
|
19033
|
-
speccedEnvelope: "OperationResultModel",
|
|
19034
|
-
markerField,
|
|
19035
|
-
markerToken,
|
|
19036
|
-
matches: items.length,
|
|
19037
|
-
truncated,
|
|
19038
|
-
note: pin.note
|
|
19039
|
-
};
|
|
19040
|
-
const numericIds = items.map((row) => {
|
|
19041
|
-
const id = row.id;
|
|
19042
|
-
if (typeof id === "number" && Number.isInteger(id) && id >= 0) return id;
|
|
19043
|
-
if (typeof id === "string" && /^\d+$/.test(id.trim())) {
|
|
19044
|
-
const parsed = Number.parseInt(id.trim(), 10);
|
|
19045
|
-
return Number.isSafeInteger(parsed) ? parsed : id.trim();
|
|
19046
|
-
}
|
|
19047
|
-
return null;
|
|
19048
|
-
});
|
|
19049
|
-
if (items.length === 1 && !truncated && numericIds[0] !== null) {
|
|
19050
|
-
const id = numericIds[0];
|
|
19051
|
-
const shapeDrift = {
|
|
19052
|
-
observedShape,
|
|
19053
|
-
speccedEnvelope: "OperationResultModel",
|
|
19054
|
-
note: `The create answered 2xx with a body that is not the specced OperationResultModel (observed: ${observedShape}); the id was recovered by ONE bounded marker read (${markerField} contains the caller's marker, limit ${MARKER_RECOVERY_LIMIT}) \u2014 exactly one numeric match.`
|
|
19055
|
-
};
|
|
19056
|
-
return { id, idFrom: "marker-recovery", shapeDrift };
|
|
19218
|
+
async searchWithDetail(query, opts = {}) {
|
|
19219
|
+
const items = await this.searchBase(query, opts);
|
|
19220
|
+
if (items.length > 0) return { items };
|
|
19221
|
+
return { items, miss: this.missDetail("contains", this.config.searchFields ?? []) };
|
|
19222
|
+
}
|
|
19223
|
+
/** The shared miss explanation for `resolveWithDetail` / `searchWithDetail`. @internal */
|
|
19224
|
+
missDetail(operator, fields) {
|
|
19225
|
+
const key = this.config.key;
|
|
19226
|
+
const probe = fields.map((field) => ({ field, op: operator, value: "" }));
|
|
19227
|
+
const blind = blindStampFor(BLIND_SURFACE_STAMPS[key]?.query, probe);
|
|
19228
|
+
const next = [];
|
|
19229
|
+
if (blind !== void 0) {
|
|
19230
|
+
next.push(`This surface is blind to records the API user does not own, so a miss is not proof of absence: ${blind.authority} returns the record when the id is known.`);
|
|
19057
19231
|
}
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19061
|
-
|
|
19062
|
-
|
|
19232
|
+
next.push(
|
|
19233
|
+
operator === "eq" ? `Matching is exact on ${fields.join(", ")} (other identifier forms were not tried); ${key}.search finds contains candidates.` : `Matching is a contains match on ${fields.join(", ")}; try a shorter or different fragment, or ${key}.resolve with the full exact value.`
|
|
19234
|
+
);
|
|
19235
|
+
return { tried: { fields: [...fields], operator }, ...blind !== void 0 ? { blindSurface: blind } : {}, next };
|
|
19236
|
+
}
|
|
19237
|
+
/** The plain value projection of `resolveDetailed` (the `resolve` floor). */
|
|
19238
|
+
async resolveBase(identifier) {
|
|
19239
|
+
return (await this.resolveDetailed(identifier)).value;
|
|
19240
|
+
}
|
|
19241
|
+
/** The items of one page of the resource's own `/query`. */
|
|
19242
|
+
async queryPage1(conditions, maxRecords) {
|
|
19243
|
+
const body = {
|
|
19244
|
+
filter: buildFilter(conditions).filter,
|
|
19245
|
+
MaxRecords: maxRecords
|
|
19246
|
+
};
|
|
19247
|
+
const res = await this.http.request({
|
|
19248
|
+
method: "POST",
|
|
19249
|
+
idempotent: true,
|
|
19250
|
+
path: `${this.resourcePath}/query`,
|
|
19251
|
+
body,
|
|
19252
|
+
operation: `${this.config.key}.resolve`
|
|
19253
|
+
});
|
|
19254
|
+
return unwrapItems(res, `${this.config.key}.resolve`).items;
|
|
19063
19255
|
}
|
|
19064
|
-
/** `PUT /V1.0/<E>/{id}` (the vendor tolerates PATCH at the same path) → `{ id }`.
|
|
19065
|
-
* For collection-only-writable resources (`collectionUpdate`, D1) the
|
|
19066
|
-
* vendor spec defines no put/patch on the item path (it is GET-only; the
|
|
19067
|
-
* item-path PUT 405s), so the confirmed update hits the COLLECTION
|
|
19068
|
-
* `PUT /V1.0/<E>` with the record id merged into the mutation body — the
|
|
19069
|
-
* vendor keys the record by the id in the body (the same convention the
|
|
19070
|
-
* parent-scoped `updateScopedRaw` PATCH uses).
|
|
19071
|
-
*
|
|
19072
|
-
* V2 partial-update merge: the vendor's collection PUT validates the FULL
|
|
19073
|
-
* model (a partial body 500s 'Missing Required Field'), so a
|
|
19074
|
-
* collectionUpdate update MERGES the caller's partial body into the
|
|
19075
|
-
* record's current full model before the PUT: ONE fresh GET serves both
|
|
19076
|
-
* the merge source and the stale-object guard's read (a `lastModifiedDate`
|
|
19077
|
-
* mismatch throws STALE_OBJECT before the PUT; the guard semantics are
|
|
19078
|
-
* unchanged — it is a no-op where the model lacks the field or the caller
|
|
19079
|
-
* supplied no token). `opts.raw === true` sends the caller body as-is
|
|
19080
|
-
* instead (the pre-merge wire; the guard keeps its own verification read
|
|
19081
|
-
* when a token is supplied).
|
|
19082
|
-
*
|
|
19083
|
-
* Issue #34: with `update.putFallback: true` (OFF by default), a TYPED
|
|
19084
|
-
* 404 on the pinned route retries ONCE as `PUT /{Entity}/{id}` where the
|
|
19085
|
-
* gate holds — never a collectionUpdate route (the spec defines no
|
|
19086
|
-
* item-path update there), never a `raw: true` update, never a non-404
|
|
19087
|
-
* fault; the retry body runs the same fresh-read merge (see
|
|
19088
|
-
* `putFallbackMergedBody`), so the fallback is never a null-clearing
|
|
19089
|
-
* partial. A declined gate (a collectionUpdate route 404s, or the retry
|
|
19090
|
-
* itself 404s) throws the self-explaining typed error (the route tried,
|
|
19091
|
-
* the status, the opt-in; `details.putFallback`). */
|
|
19092
19256
|
/**
|
|
19093
|
-
*
|
|
19094
|
-
*
|
|
19095
|
-
*
|
|
19096
|
-
*
|
|
19097
|
-
* @internal
|
|
19257
|
+
* The bounded client-scan fallback (section 19.2): an unfiltered walk of
|
|
19258
|
+
* the resource's own query endpoint, exact match on `fields`, bounded by
|
|
19259
|
+
* BOTH client caps (records AND pages). `truncated` is true when the caps
|
|
19260
|
+
* were hit before the walk completed (a complete non-match is unproven).
|
|
19098
19261
|
*/
|
|
19099
|
-
|
|
19100
|
-
|
|
19262
|
+
async scanForExactMatch(value, fields) {
|
|
19263
|
+
const caps = this.resolutionCaps;
|
|
19264
|
+
const pageSize = Math.max(1, Math.floor(caps.maxScanRecords / caps.maxScanPages));
|
|
19265
|
+
const state = { cursor: null };
|
|
19266
|
+
let scanned = 0;
|
|
19267
|
+
let pages = 0;
|
|
19268
|
+
let found = null;
|
|
19269
|
+
let exhausted = false;
|
|
19270
|
+
for (; ; ) {
|
|
19271
|
+
const page = await this.fetchPage({ maxRecords: pageSize }, pages + 1, pageSize, state);
|
|
19272
|
+
pages += 1;
|
|
19273
|
+
for (const item of page.items) {
|
|
19274
|
+
scanned += 1;
|
|
19275
|
+
const rec = item;
|
|
19276
|
+
if (fields.some((field) => rec[field] === value)) {
|
|
19277
|
+
found = item;
|
|
19278
|
+
break;
|
|
19279
|
+
}
|
|
19280
|
+
}
|
|
19281
|
+
if (found !== null) break;
|
|
19282
|
+
if (!page.hasMore) {
|
|
19283
|
+
exhausted = true;
|
|
19284
|
+
break;
|
|
19285
|
+
}
|
|
19286
|
+
if (pages >= caps.maxScanPages || scanned >= caps.maxScanRecords) break;
|
|
19287
|
+
}
|
|
19288
|
+
return { found, scanned, truncated: found === null && !exhausted };
|
|
19101
19289
|
}
|
|
19102
19290
|
/**
|
|
19103
|
-
*
|
|
19104
|
-
*
|
|
19105
|
-
*
|
|
19106
|
-
* never cached). A target miss on the merge read surfaces with the pinned
|
|
19107
|
-
* update-target reason (the issue-#14 convention) — the recovery has
|
|
19108
|
-
* nothing to merge onto, and no retry is issued.
|
|
19109
|
-
* @internal
|
|
19291
|
+
* `findBy<field>` helpers: exact (`eq`) filter on one vendor field, bounded
|
|
19292
|
+
* to 2 records — 0 → null, 1 → the record, ≥2 → ResolutionError with the
|
|
19293
|
+
* candidate ids.
|
|
19110
19294
|
*/
|
|
19111
|
-
async
|
|
19112
|
-
|
|
19113
|
-
|
|
19114
|
-
|
|
19115
|
-
|
|
19116
|
-
|
|
19117
|
-
|
|
19295
|
+
async findByField(field, value) {
|
|
19296
|
+
if (value.trim() === "") throw new AutotaskConfigError(`findBy${field}: empty value`);
|
|
19297
|
+
const matches = await this.queryEntity(
|
|
19298
|
+
`${this.resourcePath}/query`,
|
|
19299
|
+
[{ field, op: "eq", value: value.trim() }],
|
|
19300
|
+
{ maxRecords: 2 }
|
|
19301
|
+
);
|
|
19302
|
+
if (matches.length === 0) return null;
|
|
19303
|
+
if (matches.length > 1) {
|
|
19304
|
+
const ids2 = matches.map((m) => m.id).filter((id) => typeof id === "number");
|
|
19305
|
+
throw new ResolutionError(
|
|
19306
|
+
`findBy${field}: "${value}" is ambiguous on ${this.config.key} (matched ${matches.length} records)`,
|
|
19307
|
+
{ code: "RESOLUTION_AMBIGUOUS", resourceIds: ids2, details: { idRefs: ids2.map((cid) => ({ entity: this.entityName, id: cid })) } }
|
|
19308
|
+
);
|
|
19118
19309
|
}
|
|
19119
|
-
|
|
19120
|
-
merged.id = id;
|
|
19121
|
-
return merged;
|
|
19310
|
+
return matches[0];
|
|
19122
19311
|
}
|
|
19123
19312
|
/**
|
|
19124
|
-
*
|
|
19125
|
-
*
|
|
19126
|
-
*
|
|
19127
|
-
* structured detail rides under `details.putFallback`
|
|
19128
|
-
* (`{ route, status, retried, optIn }`).
|
|
19129
|
-
* @internal
|
|
19313
|
+
* `search` — the bounded contains surface (no vendor free-text search):
|
|
19314
|
+
* `contains` on every `searchFields` field (OR-grouped), `MaxRecords` =
|
|
19315
|
+
* `limit` (default 25, clamped to 1..500). Empty query → AutotaskConfigError.
|
|
19130
19316
|
*/
|
|
19131
|
-
|
|
19132
|
-
|
|
19133
|
-
|
|
19134
|
-
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
async updateGlobal(id, data, opts = {}) {
|
|
19138
|
-
const numeric = parseIdentifier(id, "id");
|
|
19139
|
-
const operation = `${this.config.key}.update`;
|
|
19140
|
-
let body = this.normalizeMutationBody(data);
|
|
19141
|
-
const collectionUpdate = this.config.collectionUpdate === true;
|
|
19142
|
-
if (collectionUpdate) {
|
|
19143
|
-
if (opts.raw !== true) {
|
|
19144
|
-
let current;
|
|
19145
|
-
try {
|
|
19146
|
-
current = await this.getGlobalUnredacted(numeric, { maxRetries: opts.maxRetries });
|
|
19147
|
-
} catch (err) {
|
|
19148
|
-
if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
|
|
19149
|
-
throw err;
|
|
19150
|
-
}
|
|
19151
|
-
this.staleObjectCheck(current, opts, operation);
|
|
19152
|
-
body = mergePartialBody(current, body);
|
|
19153
|
-
} else {
|
|
19154
|
-
await this.guardStaleBeforeMutation(numeric, opts, operation);
|
|
19155
|
-
}
|
|
19156
|
-
body.id = numeric;
|
|
19317
|
+
async searchBase(query, opts = {}) {
|
|
19318
|
+
const text = query.trim();
|
|
19319
|
+
if (text === "") throw new AutotaskConfigError("search: empty query");
|
|
19320
|
+
const fields = this.config.searchFields ?? [];
|
|
19321
|
+
if (fields.length === 0) {
|
|
19322
|
+
throw new AutotaskConfigError(`search: ${this.config.key} has no searchable field`);
|
|
19157
19323
|
}
|
|
19158
|
-
const
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
19162
|
-
|
|
19163
|
-
|
|
19164
|
-
body,
|
|
19165
|
-
operation,
|
|
19166
|
-
maxRetries: opts.maxRetries
|
|
19167
|
-
});
|
|
19168
|
-
} catch (err) {
|
|
19169
|
-
if (!this.putFallbackEligible(err)) throw err;
|
|
19170
|
-
if (collectionUpdate) {
|
|
19171
|
-
throw this.putFallbackDeclined(err, route, false);
|
|
19172
|
-
}
|
|
19173
|
-
const merged = await this.putFallbackMergedBody(
|
|
19174
|
-
() => this.getGlobalUnredacted(numeric, { maxRetries: opts.maxRetries }),
|
|
19175
|
-
body,
|
|
19176
|
-
numeric
|
|
19177
|
-
);
|
|
19178
|
-
let retry;
|
|
19179
|
-
try {
|
|
19180
|
-
retry = await this.http.request({
|
|
19181
|
-
method: "PUT",
|
|
19182
|
-
path: `${this.resourcePath}/${numeric}`,
|
|
19183
|
-
body: merged,
|
|
19184
|
-
operation,
|
|
19185
|
-
maxRetries: opts.maxRetries
|
|
19186
|
-
});
|
|
19187
|
-
} catch (rerr) {
|
|
19188
|
-
if (this.putFallbackEligible(rerr)) throw this.putFallbackDeclined(rerr, route, true);
|
|
19189
|
-
throw rerr;
|
|
19324
|
+
const limit = opts.limit === void 0 ? 25 : clampPageSize(opts.limit);
|
|
19325
|
+
const leaf = fields.map((field) => ({ field, op: "contains", value: text }));
|
|
19326
|
+
const root = leaf.length === 1 ? leaf[0] : { op: "or", items: leaf };
|
|
19327
|
+
for (const key of Object.keys(opts)) {
|
|
19328
|
+
if (key !== "limit" && key !== "companyID" && key !== "includeFields") {
|
|
19329
|
+
throw new AutotaskConfigError(`search: unknown opts key "${key}" \u2014 the bag is closed: limit, companyID, includeFields`);
|
|
19190
19330
|
}
|
|
19191
|
-
return unwrapOperation(retry, operation);
|
|
19192
19331
|
}
|
|
19193
|
-
|
|
19194
|
-
|
|
19195
|
-
|
|
19196
|
-
|
|
19197
|
-
|
|
19198
|
-
|
|
19199
|
-
|
|
19200
|
-
const
|
|
19201
|
-
const
|
|
19202
|
-
|
|
19203
|
-
|
|
19204
|
-
|
|
19205
|
-
maxRetries: opts.maxRetries
|
|
19332
|
+
if (opts.companyID !== void 0 && !isIdentifierCandidate(opts.companyID)) {
|
|
19333
|
+
throw new AutotaskConfigError(`search: companyID must be a positive integer id (got ${String(opts.companyID)})`);
|
|
19334
|
+
}
|
|
19335
|
+
if (opts.includeFields !== void 0 && (!Array.isArray(opts.includeFields) || opts.includeFields.length === 0 || opts.includeFields.some((f) => typeof f !== "string" || f === ""))) {
|
|
19336
|
+
throw new AutotaskConfigError("search: includeFields must be a non-empty array of field names");
|
|
19337
|
+
}
|
|
19338
|
+
const companyID = opts.companyID === void 0 ? void 0 : Number(opts.companyID);
|
|
19339
|
+
const includeFields = opts.includeFields === void 0 ? void 0 : opts.includeFields.includes("id") ? opts.includeFields : ["id", ...opts.includeFields];
|
|
19340
|
+
const conditions = companyID === void 0 ? [root] : [{ field: "companyID", op: "eq", value: companyID }, root];
|
|
19341
|
+
const rows = await this.queryEntity(`${this.resourcePath}/query`, conditions, {
|
|
19342
|
+
maxRecords: limit,
|
|
19343
|
+
...includeFields !== void 0 ? { includeFields } : {}
|
|
19206
19344
|
});
|
|
19207
|
-
|
|
19208
|
-
|
|
19209
|
-
|
|
19210
|
-
|
|
19211
|
-
|
|
19212
|
-
|
|
19213
|
-
|
|
19214
|
-
path: this.scopedCollectionPath(parentId),
|
|
19215
|
-
body,
|
|
19216
|
-
operation: `${this.config.key}.create`,
|
|
19217
|
-
maxRetries: opts.maxRetries
|
|
19345
|
+
if (includeFields === void 0 || includeFields.includes("userDefinedFields")) return rows;
|
|
19346
|
+
return rows.map((row) => {
|
|
19347
|
+
const r = row;
|
|
19348
|
+
if (!Array.isArray(r.userDefinedFields) || r.userDefinedFields.length > 0) return row;
|
|
19349
|
+
const rest = { ...r };
|
|
19350
|
+
delete rest.userDefinedFields;
|
|
19351
|
+
return rest;
|
|
19218
19352
|
});
|
|
19219
|
-
return this.unwrapCreateOperation(res, body, opts);
|
|
19220
19353
|
}
|
|
19221
|
-
/**
|
|
19222
|
-
*
|
|
19223
|
-
*
|
|
19224
|
-
*
|
|
19225
|
-
|
|
19226
|
-
|
|
19227
|
-
|
|
19228
|
-
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
19241
|
-
|
|
19242
|
-
|
|
19243
|
-
|
|
19244
|
-
|
|
19245
|
-
|
|
19246
|
-
|
|
19247
|
-
|
|
19248
|
-
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
});
|
|
19256
|
-
} catch (rerr) {
|
|
19257
|
-
if (this.putFallbackEligible(rerr)) throw this.putFallbackDeclined(rerr, `PUT ${path}/${numeric}`, true);
|
|
19258
|
-
throw rerr;
|
|
19259
|
-
}
|
|
19260
|
-
return unwrapOperation(retry, operation);
|
|
19354
|
+
/**
|
|
19355
|
+
* Issue #287 (D7): the bounded CONTAINS fallback of `resolveAny`'s
|
|
19356
|
+
* `match: 'auto'` — the resource's own contains surface (`searchBase`),
|
|
19357
|
+
* bounded by the resource's scan RECORD cap (the same cap that bounds the
|
|
19358
|
+
* `scanOnMiss` client scan — default 500 records; the walk's page clamp
|
|
19359
|
+
* keeps the fetch within the bounded-query page budget). Returns the
|
|
19360
|
+
* matching items (0..cap): the caller folds 1-hit / many / none. Zero wire
|
|
19361
|
+
* on an id-only resource (no primary text field → the empty set, so the
|
|
19362
|
+
* caller reports the miss unchanged).
|
|
19363
|
+
*/
|
|
19364
|
+
async resolveContainsMatches(value) {
|
|
19365
|
+
const fields = this.config.searchFields ?? [];
|
|
19366
|
+
if (fields.length === 0) return [];
|
|
19367
|
+
const limit = Math.min(this.resolutionCaps.maxScanRecords, 500);
|
|
19368
|
+
return this.searchBase(value, { limit });
|
|
19369
|
+
}
|
|
19370
|
+
};
|
|
19371
|
+
function isEmpty(value) {
|
|
19372
|
+
if (value === null || value === void 0) return true;
|
|
19373
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
19374
|
+
return typeof value === "object" && Object.keys(value).length === 0;
|
|
19375
|
+
}
|
|
19376
|
+
|
|
19377
|
+
// src/resources/base/mutations.ts
|
|
19378
|
+
var MutationBase = class extends MetaBase {
|
|
19379
|
+
// ------------------------------------------------------------------
|
|
19380
|
+
// Mutations
|
|
19381
|
+
// ------------------------------------------------------------------
|
|
19382
|
+
/** Normalise a mutation body: `id` OR `itemId` → `id`; UDF map → UDF array (UDF-capable only). */
|
|
19383
|
+
normalizeMutationBody(data) {
|
|
19384
|
+
const body = { ...data };
|
|
19385
|
+
if (body.itemId !== void 0 && body.id === void 0) {
|
|
19386
|
+
body.id = body.itemId;
|
|
19387
|
+
delete body.itemId;
|
|
19261
19388
|
}
|
|
19262
|
-
|
|
19389
|
+
if (this.config.udfCapable && body.userDefinedFields !== void 0) {
|
|
19390
|
+
body.userDefinedFields = normalizeUdfInput(body.userDefinedFields);
|
|
19391
|
+
}
|
|
19392
|
+
return body;
|
|
19263
19393
|
}
|
|
19264
|
-
/**
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
const numeric = parseIdentifier(id, "id");
|
|
19394
|
+
/** `POST /V1.0/<E>` → `{ id }` (issue #13: a drifted 2xx runs the bounded marker recovery where the plan pins a marker field). */
|
|
19395
|
+
async createGlobal(data, opts = {}) {
|
|
19396
|
+
const body = this.normalizeMutationBody(data);
|
|
19268
19397
|
const res = await this.http.request({
|
|
19269
|
-
method: "
|
|
19270
|
-
path: this.
|
|
19271
|
-
|
|
19398
|
+
method: "POST",
|
|
19399
|
+
path: this.resourcePath,
|
|
19400
|
+
body,
|
|
19401
|
+
operation: `${this.config.key}.create`,
|
|
19272
19402
|
maxRetries: opts.maxRetries
|
|
19273
19403
|
});
|
|
19274
|
-
return
|
|
19404
|
+
return this.unwrapCreateOperation(res, body, opts);
|
|
19275
19405
|
}
|
|
19276
19406
|
/**
|
|
19277
|
-
*
|
|
19278
|
-
*
|
|
19407
|
+
* Issue #13: the create-response envelope unwrap with the bounded marker
|
|
19408
|
+
* recovery. The fast path is the plain `unwrapOperation` (a well-formed
|
|
19409
|
+
* envelope costs ZERO extra requests and returns the byte-identical
|
|
19410
|
+
* `{ id }` result). Only a typed `INVALID_RESPONSE` (a 2xx body that is
|
|
19411
|
+
* not the specced envelope / `{ id }` / `{ itemId }` alternative) with a
|
|
19412
|
+
* plan-pinned marker field AND `recoverIdFromMarker !== false` runs the
|
|
19413
|
+
* fallback — and it can only run on the APPLY path: a dry run returns the
|
|
19414
|
+
* pinned envelope BEFORE the mutating request, so the fallback never sees
|
|
19415
|
+
* a dry-run response (skipped entirely by construction).
|
|
19416
|
+
* @internal
|
|
19279
19417
|
*/
|
|
19280
|
-
async
|
|
19281
|
-
const
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
|
|
19285
|
-
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
if (!isRecord2(res)) {
|
|
19290
|
-
throw new AutotaskError(
|
|
19291
|
-
`Expected the deleted attachment model for ID ${numeric}; got a different shape`,
|
|
19292
|
-
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric], details: { idRefs: [{ entity: this.entityName, id: numeric }] } }
|
|
19293
|
-
);
|
|
19418
|
+
async unwrapCreateOperation(body, wireBody, opts) {
|
|
19419
|
+
const operation = `${this.config.key}.create`;
|
|
19420
|
+
try {
|
|
19421
|
+
return unwrapOperation(body, operation);
|
|
19422
|
+
} catch (err) {
|
|
19423
|
+
if (!isAutotaskError(err) || err.code !== "INVALID_RESPONSE") throw err;
|
|
19424
|
+
const pin = RESPONSE_DRIFT_MARKERS[this.config.key];
|
|
19425
|
+
if (pin === void 0 || opts.recoverIdFromMarker === false) throw err;
|
|
19426
|
+
return this.recoverCreatedIdByMarker(err, body, wireBody, opts, pin);
|
|
19294
19427
|
}
|
|
19295
|
-
return res;
|
|
19296
19428
|
}
|
|
19297
|
-
|
|
19298
|
-
|
|
19299
|
-
|
|
19300
|
-
|
|
19301
|
-
|
|
19302
|
-
|
|
19303
|
-
|
|
19304
|
-
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
19309
|
-
|
|
19310
|
-
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
}
|
|
19319
|
-
|
|
19320
|
-
|
|
19321
|
-
}
|
|
19322
|
-
const
|
|
19323
|
-
const
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
|
|
19332
|
-
|
|
19333
|
-
|
|
19334
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
19337
|
-
|
|
19338
|
-
const firstId = first.id;
|
|
19339
|
-
if (isIdentifierCandidate(firstId)) {
|
|
19340
|
-
existing = first;
|
|
19341
|
-
existingId = parseIdentifier(firstId, "id");
|
|
19342
|
-
}
|
|
19343
|
-
}
|
|
19344
|
-
}
|
|
19345
|
-
if (cinOpts.dryRun === true) {
|
|
19346
|
-
const checks = [
|
|
19347
|
-
{ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 }
|
|
19348
|
-
];
|
|
19349
|
-
if (dedupFields.length > 0) {
|
|
19350
|
-
checks.push({ name: "duplicate-detection", ok: existingId === void 0 });
|
|
19429
|
+
/**
|
|
19430
|
+
* Issue #13: the ONE bounded marker read on the resource's own query
|
|
19431
|
+
* surface (`POST <collection>/query`, MaxRecords 5 — a read: idempotent,
|
|
19432
|
+
* zero mutating wire; the fallback never issues anything else). Outcomes:
|
|
19433
|
+
* exactly ONE numeric match → the normal success result stamped
|
|
19434
|
+
* `idFrom: 'marker-recovery'` + the `shapeDrift` note; zero matches, more
|
|
19435
|
+
* than one, or a cap-tripped search (a full 5 + a next cursor) → the
|
|
19436
|
+
* existing `INVALID_RESPONSE` with the drift detail + the failed search
|
|
19437
|
+
* (candidate ids listed — never a guessed id).
|
|
19438
|
+
* @internal
|
|
19439
|
+
*/
|
|
19440
|
+
async recoverCreatedIdByMarker(err, body, wireBody, opts, pin) {
|
|
19441
|
+
const operation = `${this.config.key}.create`;
|
|
19442
|
+
const markerField = typeof opts.markerField === "string" && opts.markerField.trim() !== "" ? opts.markerField.trim() : pin.markerField;
|
|
19443
|
+
const markerToken = typeof opts.markerToken === "string" && opts.markerToken.trim() !== "" ? opts.markerToken.trim() : defaultMarkerToken(wireBody);
|
|
19444
|
+
if (markerToken === null) throw err;
|
|
19445
|
+
const observedShape = describeResponseShape(body);
|
|
19446
|
+
const res = await this.http.request({
|
|
19447
|
+
method: "POST",
|
|
19448
|
+
idempotent: true,
|
|
19449
|
+
path: `${this.resourcePath}/query`,
|
|
19450
|
+
body: { MaxRecords: MARKER_RECOVERY_LIMIT, filter: [{ field: markerField, op: "contains", value: markerToken }] },
|
|
19451
|
+
operation: `${operation}.marker-recovery`
|
|
19452
|
+
});
|
|
19453
|
+
const { items, pageDetails } = unwrapItems(res, operation);
|
|
19454
|
+
const truncated = items.length >= MARKER_RECOVERY_LIMIT && pageDetails.nextPageUrl != null;
|
|
19455
|
+
const drift = {
|
|
19456
|
+
observedShape,
|
|
19457
|
+
speccedEnvelope: "OperationResultModel",
|
|
19458
|
+
markerField,
|
|
19459
|
+
markerToken,
|
|
19460
|
+
matches: items.length,
|
|
19461
|
+
truncated,
|
|
19462
|
+
note: pin.note
|
|
19463
|
+
};
|
|
19464
|
+
const numericIds = items.map((row) => {
|
|
19465
|
+
const id = row.id;
|
|
19466
|
+
if (typeof id === "number" && Number.isInteger(id) && id >= 0) return id;
|
|
19467
|
+
if (typeof id === "string" && /^\d+$/.test(id.trim())) {
|
|
19468
|
+
const parsed = Number.parseInt(id.trim(), 10);
|
|
19469
|
+
return Number.isSafeInteger(parsed) ? parsed : id.trim();
|
|
19351
19470
|
}
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19355
|
-
|
|
19356
|
-
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19363
|
-
}
|
|
19364
|
-
if (existing !== void 0 && existingId !== void 0) {
|
|
19365
|
-
return { id: existingId, created: false, existing };
|
|
19471
|
+
return null;
|
|
19472
|
+
});
|
|
19473
|
+
if (items.length === 1 && !truncated && numericIds[0] !== null) {
|
|
19474
|
+
const id = numericIds[0];
|
|
19475
|
+
const shapeDrift = {
|
|
19476
|
+
observedShape,
|
|
19477
|
+
speccedEnvelope: "OperationResultModel",
|
|
19478
|
+
note: `The create answered 2xx with a body that is not the specced OperationResultModel (observed: ${observedShape}); the id was recovered by ONE bounded marker read (${markerField} contains the caller's marker, limit ${MARKER_RECOVERY_LIMIT}) \u2014 exactly one numeric match.`
|
|
19479
|
+
};
|
|
19480
|
+
return { id, idFrom: "marker-recovery", shapeDrift };
|
|
19366
19481
|
}
|
|
19367
|
-
const
|
|
19368
|
-
|
|
19482
|
+
const candidateIds = numericIds.map((n, i) => n ?? String(items[i].id ?? "unknown"));
|
|
19483
|
+
const outcome = items.length === 0 ? "found 0 matches" : truncated ? `found ${items.length} matches at the limit ${MARKER_RECOVERY_LIMIT} cap (truncated \u2014 more may exist)` : `found ${items.length} matches`;
|
|
19484
|
+
err.details = { ...err.details, responseDrift: { ...drift, candidateIds } };
|
|
19485
|
+
err.suggestedAction = `The create answered 2xx with a body that is not the specced OperationResultModel (observed shape: ${observedShape}); the bounded marker recovery (${markerField} contains the caller's marker, limit ${MARKER_RECOVERY_LIMIT}) ${outcome} \u2014 ` + (items.length === 0 ? "the record may exist with an unfindable marker; report the vendor response-shape drift and locate the record in the UI." : "refusing to guess an id \u2014 disambiguate with an exact-field query (or the UI) and recover the id before retrying.");
|
|
19486
|
+
throw err;
|
|
19369
19487
|
}
|
|
19370
|
-
|
|
19371
|
-
|
|
19372
|
-
|
|
19488
|
+
/** `PUT /V1.0/<E>/{id}` (the vendor tolerates PATCH at the same path) → `{ id }`.
|
|
19489
|
+
* For collection-only-writable resources (`collectionUpdate`, D1) the
|
|
19490
|
+
* vendor spec defines no put/patch on the item path (it is GET-only; the
|
|
19491
|
+
* item-path PUT 405s), so the confirmed update hits the COLLECTION
|
|
19492
|
+
* `PUT /V1.0/<E>` with the record id merged into the mutation body — the
|
|
19493
|
+
* vendor keys the record by the id in the body (the same convention the
|
|
19494
|
+
* parent-scoped `updateScopedRaw` PATCH uses).
|
|
19495
|
+
*
|
|
19496
|
+
* V2 partial-update merge: the vendor's collection PUT validates the FULL
|
|
19497
|
+
* model (a partial body 500s 'Missing Required Field'), so a
|
|
19498
|
+
* collectionUpdate update MERGES the caller's partial body into the
|
|
19499
|
+
* record's current full model before the PUT: ONE fresh GET serves both
|
|
19500
|
+
* the merge source and the stale-object guard's read (a `lastModifiedDate`
|
|
19501
|
+
* mismatch throws STALE_OBJECT before the PUT; the guard semantics are
|
|
19502
|
+
* unchanged — it is a no-op where the model lacks the field or the caller
|
|
19503
|
+
* supplied no token). `opts.raw === true` sends the caller body as-is
|
|
19504
|
+
* instead (the pre-merge wire; the guard keeps its own verification read
|
|
19505
|
+
* when a token is supplied).
|
|
19506
|
+
*
|
|
19507
|
+
* Issue #34: with `update.putFallback: true` (OFF by default), a TYPED
|
|
19508
|
+
* 404 on the pinned route retries ONCE as `PUT /{Entity}/{id}` where the
|
|
19509
|
+
* gate holds — never a collectionUpdate route (the spec defines no
|
|
19510
|
+
* item-path update there), never a `raw: true` update, never a non-404
|
|
19511
|
+
* fault; the retry body runs the same fresh-read merge (see
|
|
19512
|
+
* `putFallbackMergedBody`), so the fallback is never a null-clearing
|
|
19513
|
+
* partial. A declined gate (a collectionUpdate route 404s, or the retry
|
|
19514
|
+
* itself 404s) throws the self-explaining typed error (the route tried,
|
|
19515
|
+
* the status, the opt-in; `details.putFallback`). */
|
|
19373
19516
|
/**
|
|
19374
|
-
*
|
|
19375
|
-
*
|
|
19376
|
-
*
|
|
19377
|
-
*
|
|
19378
|
-
*
|
|
19379
|
-
*/
|
|
19380
|
-
|
|
19381
|
-
|
|
19382
|
-
const expected = opts.lastModifiedDate;
|
|
19383
|
-
if (field === void 0 || expected === void 0) return false;
|
|
19384
|
-
const actual = current[field];
|
|
19385
|
-
if (typeof actual !== "string") return false;
|
|
19386
|
-
if (actual !== expected) {
|
|
19387
|
-
throw new StaleObjectError(
|
|
19388
|
-
`stale object: ${this.config.key} ${field} changed since the caller read it (expected ${expected}, current ${actual})`,
|
|
19389
|
-
{
|
|
19390
|
-
code: "STALE_OBJECT",
|
|
19391
|
-
operation,
|
|
19392
|
-
suggestedAction: "Re-read the record and retry with the fresh lastModifiedDate."
|
|
19393
|
-
}
|
|
19394
|
-
);
|
|
19395
|
-
}
|
|
19396
|
-
return true;
|
|
19517
|
+
* Issue #34 gate: the fallback may fire only while `update.putFallback`
|
|
19518
|
+
* is ON and the pinned update route answered a TYPED 404 (a transport
|
|
19519
|
+
* `NotFoundError` with `httpStatus: 404`) — never a 400/405/422/5xx, and
|
|
19520
|
+
* never a 2xx shape fault (those keep today's typed error byte-identical).
|
|
19521
|
+
* @internal
|
|
19522
|
+
*/
|
|
19523
|
+
putFallbackEligible(err) {
|
|
19524
|
+
return this.http.update.putFallback === true && err instanceof NotFoundError && err.httpStatus === 404;
|
|
19397
19525
|
}
|
|
19398
19526
|
/**
|
|
19399
|
-
*
|
|
19400
|
-
*
|
|
19401
|
-
*
|
|
19402
|
-
*
|
|
19527
|
+
* Issue #34: the fallback retry body — the caller's (normalised) fields
|
|
19528
|
+
* merged onto a FRESH unredacted read of the current record (the same
|
|
19529
|
+
* merge the collection-update path performs; the read is the live record,
|
|
19530
|
+
* never cached). A target miss on the merge read surfaces with the pinned
|
|
19531
|
+
* update-target reason (the issue-#14 convention) — the recovery has
|
|
19532
|
+
* nothing to merge onto, and no retry is issued.
|
|
19533
|
+
* @internal
|
|
19403
19534
|
*/
|
|
19404
|
-
async
|
|
19405
|
-
if (opts.lastModifiedDate === void 0 || this.config.staleField === void 0) return;
|
|
19535
|
+
async putFallbackMergedBody(read, callerBody, id) {
|
|
19406
19536
|
let current;
|
|
19407
19537
|
try {
|
|
19408
|
-
current =
|
|
19538
|
+
current = await read();
|
|
19409
19539
|
} catch (err) {
|
|
19410
19540
|
if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
|
|
19411
19541
|
throw err;
|
|
19412
19542
|
}
|
|
19413
|
-
|
|
19543
|
+
const merged = mergePartialBody(current, callerBody);
|
|
19544
|
+
merged.id = id;
|
|
19545
|
+
return merged;
|
|
19414
19546
|
}
|
|
19415
19547
|
/**
|
|
19416
|
-
*
|
|
19417
|
-
*
|
|
19418
|
-
*
|
|
19419
|
-
*
|
|
19548
|
+
* Issue #34: the self-explaining typed error a DECLINED gate throws. The
|
|
19549
|
+
* original transport error keeps its identity (class / code / httpStatus);
|
|
19550
|
+
* the message names the route tried, the status and the opt-in, and the
|
|
19551
|
+
* structured detail rides under `details.putFallback`
|
|
19552
|
+
* (`{ route, status, retried, optIn }`).
|
|
19553
|
+
* @internal
|
|
19420
19554
|
*/
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
}
|
|
19430
|
-
let
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
19435
|
-
throw new AutotaskConfigError(
|
|
19436
|
-
`dry-run: ${this.config.key} is parent-scoped but has no parentPath configured`
|
|
19437
|
-
);
|
|
19438
|
-
}
|
|
19439
|
-
let parent = null;
|
|
19440
|
-
if (isIdentifierCandidate(parentId)) {
|
|
19555
|
+
putFallbackDeclined(err, route, retried) {
|
|
19556
|
+
err.message = `${route} answered 404 on the pinned update route; ` + (retried ? `the update.putFallback retry ${route} also answered 404 \u2014 the zone drops the item route as well, so no further recovery is possible` : `the opt-in update.putFallback declines to retry this route (collectionUpdate resources have no spec-defined item-path update; raw updates send exactly what they were told) \u2014 no fallback was issued`);
|
|
19557
|
+
err.details = { ...err.details ?? {}, putFallback: { route, status: 404, retried, optIn: "update.putFallback" } };
|
|
19558
|
+
err.suggestedAction = retried ? "The zone answers 404 for both the pinned update route and the item route \u2014 verify the zone route registration for this entity before retrying." : "The zone drops the pinned update route and the spec defines no item-path update for this resource \u2014 verify the zone route registration, or issue the update against the collection route raw.";
|
|
19559
|
+
return err;
|
|
19560
|
+
}
|
|
19561
|
+
async updateGlobal(id, data, opts = {}) {
|
|
19562
|
+
const numeric = parseIdentifier(id, "id");
|
|
19563
|
+
const operation = `${this.config.key}.update`;
|
|
19564
|
+
let body = this.normalizeMutationBody(data);
|
|
19565
|
+
const collectionUpdate = this.config.collectionUpdate === true;
|
|
19566
|
+
if (collectionUpdate) {
|
|
19567
|
+
if (opts.raw !== true) {
|
|
19568
|
+
let current;
|
|
19441
19569
|
try {
|
|
19442
|
-
|
|
19443
|
-
parentPath,
|
|
19444
|
-
parseIdentifier(parentId, "parentId"),
|
|
19445
|
-
true
|
|
19446
|
-
);
|
|
19570
|
+
current = await this.getGlobalUnredacted(numeric, { maxRetries: opts.maxRetries });
|
|
19447
19571
|
} catch (err) {
|
|
19448
|
-
if (
|
|
19572
|
+
if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
|
|
19573
|
+
throw err;
|
|
19449
19574
|
}
|
|
19575
|
+
this.staleObjectCheck(current, opts, operation);
|
|
19576
|
+
body = mergePartialBody(current, body);
|
|
19577
|
+
} else {
|
|
19578
|
+
await this.guardStaleBeforeMutation(numeric, opts, operation);
|
|
19450
19579
|
}
|
|
19451
|
-
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
operation: `${this.config.key}.create`,
|
|
19468
|
-
method: "POST",
|
|
19469
|
-
path,
|
|
19470
|
-
ids: [],
|
|
19471
|
-
checks,
|
|
19472
|
-
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19473
|
-
warnings,
|
|
19474
|
-
data
|
|
19475
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19476
|
-
}
|
|
19477
|
-
/**
|
|
19478
|
-
* `update` dry-run: input validation + current-record fetch (a missing
|
|
19479
|
-
* record carries `warnings` and no `diff` — pinned), the stale-object
|
|
19480
|
-
* check (mismatch throws STALE_OBJECT) and the best-effort `diff`. NO
|
|
19481
|
-
* mutating request is issued.
|
|
19482
|
-
*/
|
|
19483
|
-
async runUpdateDryRun(id, data, opts = {}, parentId) {
|
|
19484
|
-
const operation = `${this.config.key}.update`;
|
|
19485
|
-
const checks = [];
|
|
19486
|
-
const warnings = [];
|
|
19487
|
-
checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
|
|
19488
|
-
const scopedUpdate = parentId !== void 0 && this.config.parentScopedPath !== void 0;
|
|
19489
|
-
const collectionUpdate = scopedUpdate || this.config.collectionUpdate === true;
|
|
19490
|
-
const path = scopedUpdate ? this.scopedCollectionPath(parentId) : collectionUpdate ? this.resourcePath : `${this.resourcePath}/${id}`;
|
|
19491
|
-
const method = scopedUpdate ? "PATCH" : "PUT";
|
|
19492
|
-
let current = null;
|
|
19493
|
-
try {
|
|
19494
|
-
current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
|
|
19495
|
-
} catch (err) {
|
|
19496
|
-
if (err instanceof NotFoundError) {
|
|
19497
|
-
const updateReason = EMPTY_REASON_STAMPS[this.config.key]?.update;
|
|
19498
|
-
checks.push({
|
|
19499
|
-
name: "current-record-exists",
|
|
19500
|
-
ok: false,
|
|
19501
|
-
...updateReason !== void 0 ? { note: updateReason.note } : {}
|
|
19502
|
-
});
|
|
19503
|
-
warnings.push(`current record ${id} not fetchable; no diff`);
|
|
19504
|
-
return buildDryRunResult({
|
|
19505
|
-
operation,
|
|
19506
|
-
method,
|
|
19507
|
-
path,
|
|
19508
|
-
ids: [id],
|
|
19509
|
-
checks,
|
|
19510
|
-
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19511
|
-
warnings,
|
|
19512
|
-
data,
|
|
19513
|
-
...updateReason !== void 0 ? { emptyReason: updateReason } : {}
|
|
19514
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19515
|
-
}
|
|
19516
|
-
throw err;
|
|
19517
|
-
}
|
|
19518
|
-
checks.push({ name: "current-record-exists", ok: true });
|
|
19519
|
-
if (this.staleObjectCheck(current, opts, operation)) {
|
|
19520
|
-
checks.push({ name: "stale-check", ok: true });
|
|
19521
|
-
}
|
|
19522
|
-
let envelopeData = data;
|
|
19523
|
-
if (parentId === void 0 && this.config.collectionUpdate === true && opts.raw !== true) {
|
|
19524
|
-
envelopeData = mergePartialBody(
|
|
19525
|
-
current,
|
|
19526
|
-
this.normalizeMutationBody(data)
|
|
19527
|
-
);
|
|
19528
|
-
}
|
|
19529
|
-
return buildDryRunResult({
|
|
19530
|
-
operation,
|
|
19531
|
-
method,
|
|
19532
|
-
path,
|
|
19533
|
-
ids: [id],
|
|
19534
|
-
checks,
|
|
19535
|
-
diff: computeFieldDiff(current, data),
|
|
19536
|
-
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19537
|
-
warnings,
|
|
19538
|
-
data: envelopeData
|
|
19539
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19540
|
-
}
|
|
19541
|
-
/**
|
|
19542
|
-
* `delete` dry-run: current-record verification + the stale-object check +
|
|
19543
|
-
* the pinned envelope (no diff — a delete has no before/after fields). NO
|
|
19544
|
-
* mutating request is issued.
|
|
19545
|
-
*/
|
|
19546
|
-
async runDeleteDryRun(id, opts = {}, parentId) {
|
|
19547
|
-
const operation = `${this.config.key}.delete`;
|
|
19548
|
-
const checks = [];
|
|
19549
|
-
const warnings = [];
|
|
19550
|
-
const path = parentId === void 0 ? `${this.resourcePath}/${id}` : this.scopedItemPath(parentId, id);
|
|
19551
|
-
let current = null;
|
|
19552
|
-
try {
|
|
19553
|
-
current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
|
|
19554
|
-
} catch (err) {
|
|
19555
|
-
if (err instanceof NotFoundError) {
|
|
19556
|
-
checks.push({ name: "current-record-exists", ok: false });
|
|
19557
|
-
warnings.push(`record ${id} not fetchable; the vendor would reject the delete`);
|
|
19558
|
-
return buildDryRunResult({
|
|
19559
|
-
operation,
|
|
19560
|
-
method: "DELETE",
|
|
19561
|
-
path,
|
|
19562
|
-
ids: [id],
|
|
19563
|
-
checks,
|
|
19564
|
-
impact: { affected: 1, scope: "single-record", reversible: false },
|
|
19565
|
-
warnings
|
|
19566
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19567
|
-
}
|
|
19568
|
-
throw err;
|
|
19569
|
-
}
|
|
19570
|
-
checks.push({ name: "current-record-exists", ok: true });
|
|
19571
|
-
if (this.staleObjectCheck(current, opts, operation)) {
|
|
19572
|
-
checks.push({ name: "stale-check", ok: true });
|
|
19573
|
-
}
|
|
19574
|
-
return buildDryRunResult({
|
|
19575
|
-
operation,
|
|
19576
|
-
method: "DELETE",
|
|
19577
|
-
path,
|
|
19578
|
-
ids: [id],
|
|
19579
|
-
checks,
|
|
19580
|
-
impact: { affected: 1, scope: "single-record", reversible: false },
|
|
19581
|
-
warnings
|
|
19582
|
-
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19583
|
-
}
|
|
19584
|
-
};
|
|
19585
|
-
|
|
19586
|
-
// src/resources/base/BaseResource.ts
|
|
19587
|
-
var BaseResource = class extends MutationBase {
|
|
19588
|
-
};
|
|
19589
|
-
|
|
19590
|
-
// src/operations/compositeNames.ts
|
|
19591
|
-
var PICKLIST_VALUE_NOT_SERVED_NOTE = "The picklist is served to this API user, but this record's value is not in it (an inactive or unexposed value) \u2014 the label cannot be resolved and is null; refreshing the field catalog will not surface it. (A picklist the vendor does not expose at all is reported with the field-catalog honesty note instead.)";
|
|
19592
|
-
var PICKLIST_CATALOG_UNAVAILABLE_NOTE = "The field-catalog read that serves picklist labels failed for this call, so this label is unavailable (null), not absent: the value may well have one. Retry the call once the catalog read succeeds (see names.catalogError).";
|
|
19593
|
-
var DEFAULT_MAX_NAME_READS = 20;
|
|
19594
|
-
var TICKET_LABEL_FIELDS = ["status", "priority", "ticketType", "issueType", "subIssueType", "source", "queueID"];
|
|
19595
|
-
function isVendorId(value) {
|
|
19596
|
-
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0;
|
|
19597
|
-
if (typeof value === "string") return /^\d+$/.test(value);
|
|
19598
|
-
return false;
|
|
19599
|
-
}
|
|
19600
|
-
var zeroIsRecord = (table) => table === "company";
|
|
19601
|
-
function createNamePlan(labelFields = [], labelResource) {
|
|
19602
|
-
const resourceIds = [];
|
|
19603
|
-
const resSeen = /* @__PURE__ */ new Set();
|
|
19604
|
-
const recordFks = [];
|
|
19605
|
-
const fkSeen = /* @__PURE__ */ new Set();
|
|
19606
|
-
let lineOfBusinessId;
|
|
19607
|
-
let lineOfBusinessSource;
|
|
19608
|
-
let lineOfBusinessContractId;
|
|
19609
|
-
return {
|
|
19610
|
-
plan: () => ({
|
|
19611
|
-
resourceIds,
|
|
19612
|
-
recordFks,
|
|
19613
|
-
labelFields,
|
|
19614
|
-
...labelResource !== void 0 ? { labelResource } : {},
|
|
19615
|
-
...lineOfBusinessId !== void 0 ? { lineOfBusinessId } : {},
|
|
19616
|
-
...lineOfBusinessSource !== void 0 ? { lineOfBusinessSource } : {},
|
|
19617
|
-
...lineOfBusinessContractId !== void 0 ? { lineOfBusinessContractId } : {}
|
|
19618
|
-
}),
|
|
19619
|
-
addResource: (value) => {
|
|
19620
|
-
if (!isVendorId(value) || Number(value) === 0) return;
|
|
19621
|
-
const k = String(value);
|
|
19622
|
-
if (!resSeen.has(k)) {
|
|
19623
|
-
resSeen.add(k);
|
|
19624
|
-
resourceIds.push(value);
|
|
19625
|
-
}
|
|
19626
|
-
},
|
|
19627
|
-
addFk: (table, value) => {
|
|
19628
|
-
if (!isVendorId(value) || Number(value) === 0 && !zeroIsRecord(table)) return;
|
|
19629
|
-
const k = `${table}:${String(value)}`;
|
|
19630
|
-
if (!fkSeen.has(k)) {
|
|
19631
|
-
fkSeen.add(k);
|
|
19632
|
-
recordFks.push({ table, id: value });
|
|
19580
|
+
body.id = numeric;
|
|
19581
|
+
}
|
|
19582
|
+
const route = collectionUpdate ? `PUT ${this.resourcePath}` : `PUT ${this.resourcePath}/${numeric}`;
|
|
19583
|
+
let res;
|
|
19584
|
+
try {
|
|
19585
|
+
res = await this.http.request({
|
|
19586
|
+
method: "PUT",
|
|
19587
|
+
path: collectionUpdate ? this.resourcePath : `${this.resourcePath}/${numeric}`,
|
|
19588
|
+
body,
|
|
19589
|
+
operation,
|
|
19590
|
+
maxRetries: opts.maxRetries
|
|
19591
|
+
});
|
|
19592
|
+
} catch (err) {
|
|
19593
|
+
if (!this.putFallbackEligible(err)) throw err;
|
|
19594
|
+
if (collectionUpdate) {
|
|
19595
|
+
throw this.putFallbackDeclined(err, route, false);
|
|
19633
19596
|
}
|
|
19634
|
-
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
|
|
19639
|
-
|
|
19640
|
-
|
|
19641
|
-
|
|
19642
|
-
|
|
19643
|
-
|
|
19644
|
-
|
|
19597
|
+
const merged = await this.putFallbackMergedBody(
|
|
19598
|
+
() => this.getGlobalUnredacted(numeric, { maxRetries: opts.maxRetries }),
|
|
19599
|
+
body,
|
|
19600
|
+
numeric
|
|
19601
|
+
);
|
|
19602
|
+
let retry;
|
|
19603
|
+
try {
|
|
19604
|
+
retry = await this.http.request({
|
|
19605
|
+
method: "PUT",
|
|
19606
|
+
path: `${this.resourcePath}/${numeric}`,
|
|
19607
|
+
body: merged,
|
|
19608
|
+
operation,
|
|
19609
|
+
maxRetries: opts.maxRetries
|
|
19610
|
+
});
|
|
19611
|
+
} catch (rerr) {
|
|
19612
|
+
if (this.putFallbackEligible(rerr)) throw this.putFallbackDeclined(rerr, route, true);
|
|
19613
|
+
throw rerr;
|
|
19645
19614
|
}
|
|
19615
|
+
return unwrapOperation(retry, operation);
|
|
19646
19616
|
}
|
|
19647
|
-
|
|
19648
|
-
}
|
|
19649
|
-
function addNameSibling(row, field, resolve) {
|
|
19650
|
-
const value = row[field];
|
|
19651
|
-
if (value === void 0) return;
|
|
19652
|
-
row[`${field}Name`] = isVendorId(value) ? resolve(value) : null;
|
|
19653
|
-
}
|
|
19654
|
-
function addLabelSibling(row, field, names) {
|
|
19655
|
-
const value = row[field];
|
|
19656
|
-
if (value === void 0) return;
|
|
19657
|
-
row[`${field}Label`] = names.labelFor(field, value);
|
|
19658
|
-
}
|
|
19659
|
-
var FK_TABLE_PATH = {
|
|
19660
|
-
role: "/V1.0/Roles",
|
|
19661
|
-
billingCode: "/V1.0/BillingCodes",
|
|
19662
|
-
contract: "/V1.0/Contracts",
|
|
19663
|
-
company: "/V1.0/Companies",
|
|
19664
|
-
ticket: "/V1.0/Tickets"
|
|
19665
|
-
};
|
|
19666
|
-
var FK_NAME_FIELD = {
|
|
19667
|
-
role: "name",
|
|
19668
|
-
billingCode: "name",
|
|
19669
|
-
contract: "contractName",
|
|
19670
|
-
company: "companyName",
|
|
19671
|
-
ticket: "ticketNumber"
|
|
19672
|
-
};
|
|
19673
|
-
function fkName(record, table) {
|
|
19674
|
-
if (table === "ticket") {
|
|
19675
|
-
const parts = [record["ticketNumber"], record["title"]].filter((v) => typeof v === "string" && v.trim() !== "").map((v) => v.trim());
|
|
19676
|
-
return parts.length > 0 ? parts.join(" ") : null;
|
|
19677
|
-
}
|
|
19678
|
-
const value = record[FK_NAME_FIELD[table]];
|
|
19679
|
-
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
19680
|
-
}
|
|
19681
|
-
function faultRow(err) {
|
|
19682
|
-
const e = err;
|
|
19683
|
-
return e.httpStatus !== void 0 ? { code: e.code, message: e.message, httpStatus: e.httpStatus } : { code: e.code, message: e.message };
|
|
19684
|
-
}
|
|
19685
|
-
function vendorId(value) {
|
|
19686
|
-
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
|
|
19687
|
-
if (typeof value === "string" && /^[0-9]+$/.test(value.trim())) {
|
|
19688
|
-
const n = Number(value.trim());
|
|
19689
|
-
return Number.isSafeInteger(n) && n >= 1 ? n : void 0;
|
|
19617
|
+
return unwrapOperation(res, operation);
|
|
19690
19618
|
}
|
|
19691
|
-
|
|
19692
|
-
|
|
19693
|
-
|
|
19694
|
-
|
|
19695
|
-
|
|
19696
|
-
|
|
19697
|
-
const res = http.request({
|
|
19698
|
-
|
|
19699
|
-
|
|
19700
|
-
|
|
19701
|
-
|
|
19702
|
-
if (!degrade) {
|
|
19703
|
-
return res.catch((e) => {
|
|
19704
|
-
if (e instanceof NotFoundError) return null;
|
|
19705
|
-
throw e;
|
|
19706
|
-
});
|
|
19707
|
-
}
|
|
19708
|
-
return res.catch((e) => {
|
|
19709
|
-
if (e instanceof NotFoundError || isAutotaskError(e) && e.code !== "ABORTED") return null;
|
|
19710
|
-
throw e;
|
|
19619
|
+
/** `DELETE /V1.0/<E>/{id}` → `{ id }` (OperationResultModel). A blank 2xx
|
|
19620
|
+
* (204 / empty body) is a valid vendor delete answer — synthesize the
|
|
19621
|
+
* result from the path id (issue #25: the transport returns undefined
|
|
19622
|
+
* for it; a non-blank wrong shape still throws via unwrapOperation). */
|
|
19623
|
+
async deleteGlobal(id, opts = {}) {
|
|
19624
|
+
const numeric = parseIdentifier(id, "id");
|
|
19625
|
+
const res = await this.http.request({
|
|
19626
|
+
method: "DELETE",
|
|
19627
|
+
path: `${this.resourcePath}/${numeric}`,
|
|
19628
|
+
operation: `${this.config.key}.delete`,
|
|
19629
|
+
maxRetries: opts.maxRetries
|
|
19711
19630
|
});
|
|
19712
|
-
|
|
19713
|
-
let remaining = options.budget;
|
|
19714
|
-
if (remaining < 1) {
|
|
19715
|
-
return {
|
|
19716
|
-
lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: "capped", organizationalLevel2: "capped", organizationalLevel1: "capped" } },
|
|
19717
|
-
used: 0,
|
|
19718
|
-
truncated: true
|
|
19719
|
-
};
|
|
19720
|
-
}
|
|
19721
|
-
remaining -= 1;
|
|
19722
|
-
const association = await get(`/V1.0/OrganizationalLevelAssociations/${associationId}`);
|
|
19723
|
-
const associationMarker = association === null ? "unresolved" : "ok";
|
|
19724
|
-
if (association === null) {
|
|
19725
|
-
return {
|
|
19726
|
-
lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: associationMarker, organizationalLevel2: "missing", organizationalLevel1: "missing" } },
|
|
19727
|
-
used: 1,
|
|
19728
|
-
truncated: false
|
|
19729
|
-
};
|
|
19730
|
-
}
|
|
19731
|
-
const l2Id = vendorId(association.organizationalLevel2ID);
|
|
19732
|
-
const l1Id = vendorId(association.organizationalLevel1ID);
|
|
19733
|
-
let l2Marker = l2Id !== void 0 ? "unresolved" : "missing";
|
|
19734
|
-
let l1Marker = l1Id !== void 0 ? "unresolved" : "missing";
|
|
19735
|
-
const need = (l2Id !== void 0 ? 1 : 0) + (l1Id !== void 0 ? 1 : 0);
|
|
19736
|
-
let l2 = null;
|
|
19737
|
-
let l1 = null;
|
|
19738
|
-
let used = 1;
|
|
19739
|
-
let truncated = false;
|
|
19740
|
-
if (need > 0 && remaining < need) {
|
|
19741
|
-
truncated = true;
|
|
19742
|
-
if (l2Id !== void 0) l2Marker = "capped";
|
|
19743
|
-
if (l1Id !== void 0) l1Marker = "capped";
|
|
19744
|
-
} else {
|
|
19745
|
-
used += need;
|
|
19746
|
-
const [r2, r1] = await Promise.all([
|
|
19747
|
-
l2Id !== void 0 ? get(`/V1.0/OrganizationalLevel2s/${l2Id}`) : Promise.resolve(null),
|
|
19748
|
-
l1Id !== void 0 ? get(`/V1.0/OrganizationalLevel1s/${l1Id}`) : Promise.resolve(null)
|
|
19749
|
-
]);
|
|
19750
|
-
l2 = r2;
|
|
19751
|
-
l1 = r1;
|
|
19752
|
-
if (l2Id !== void 0) l2Marker = l2 === null ? "unresolved" : "ok";
|
|
19753
|
-
if (l1Id !== void 0) l1Marker = l1 === null ? "unresolved" : "ok";
|
|
19754
|
-
}
|
|
19755
|
-
return {
|
|
19756
|
-
lineOfBusiness: { association, organizationalLevel2: l2, organizationalLevel1: l1, markers: { association: associationMarker, organizationalLevel2: l2Marker, organizationalLevel1: l1Marker } },
|
|
19757
|
-
used,
|
|
19758
|
-
truncated
|
|
19759
|
-
};
|
|
19760
|
-
}
|
|
19761
|
-
async function resolveCompositeNames(http, plan, opts = {}) {
|
|
19762
|
-
if (opts.maxNameReads !== void 0 && (!Number.isInteger(opts.maxNameReads) || opts.maxNameReads < 1)) {
|
|
19763
|
-
throw new AutotaskConfigError(`composite names: maxNameReads must be an integer >= 1 (got ${String(opts.maxNameReads)})`);
|
|
19764
|
-
}
|
|
19765
|
-
const budget = Math.min(DEFAULT_MAX_NAME_READS, opts.maxNameReads ?? DEFAULT_MAX_NAME_READS);
|
|
19766
|
-
const client = http.client;
|
|
19767
|
-
if (client === void 0) {
|
|
19768
|
-
throw new AutotaskConfigError("composite names: the transport is not wired to an AutotaskClient (the composite must run through the client)");
|
|
19769
|
-
}
|
|
19770
|
-
const readCache = http.readCache;
|
|
19771
|
-
const section = { truncated: false, rows: {}, reasons: {} };
|
|
19772
|
-
let remaining = budget;
|
|
19773
|
-
let contractLobPointer;
|
|
19774
|
-
let contractLobUnread;
|
|
19775
|
-
const coldResource = [];
|
|
19776
|
-
for (const id of plan.resourceIds) {
|
|
19777
|
-
const path = `/V1.0/Resources/${id}`;
|
|
19778
|
-
if (readCache !== void 0 && (readCache.peek(path).hit || readCache.peek(contextAbsentKey(path)).hit)) continue;
|
|
19779
|
-
coldResource.push(id);
|
|
19631
|
+
return res === void 0 ? { id: numeric } : unwrapOperation(res, `${this.config.key}.delete`);
|
|
19780
19632
|
}
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19633
|
+
/** Parent-scoped create: `POST .../{parentId}/<Child>` (issue #13: a drifted 2xx runs the bounded marker recovery where the plan pins a marker field). */
|
|
19634
|
+
async createScopedRaw(parentId, data, opts = {}) {
|
|
19635
|
+
const body = this.normalizeMutationBody(data);
|
|
19636
|
+
const res = await this.http.request({
|
|
19637
|
+
method: "POST",
|
|
19638
|
+
path: this.scopedCollectionPath(parentId),
|
|
19639
|
+
body,
|
|
19640
|
+
operation: `${this.config.key}.create`,
|
|
19641
|
+
maxRetries: opts.maxRetries
|
|
19642
|
+
});
|
|
19643
|
+
return this.unwrapCreateOperation(res, body, opts);
|
|
19787
19644
|
}
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19645
|
+
/** Parent-scoped update: `PATCH .../{parentId}/<Child>` — the COLLECTION.
|
|
19646
|
+
* Swagger documents put+patch on the scoped collection and has NO put/patch on
|
|
19647
|
+
* the item path anywhere in the spec; the node strips the id from the path and
|
|
19648
|
+
* PATCHes the collection, and the vendor keys the record by the id in the body. */
|
|
19649
|
+
async updateScopedRaw(parentId, id, data, opts = {}) {
|
|
19650
|
+
const numeric = parseIdentifier(id, "id");
|
|
19651
|
+
const body = this.normalizeMutationBody(data);
|
|
19652
|
+
body.id = numeric;
|
|
19653
|
+
const path = this.scopedCollectionPath(parentId);
|
|
19654
|
+
const operation = `${this.config.key}.update`;
|
|
19655
|
+
let res;
|
|
19656
|
+
try {
|
|
19657
|
+
res = await this.http.request({
|
|
19658
|
+
method: "PATCH",
|
|
19659
|
+
path,
|
|
19660
|
+
body,
|
|
19661
|
+
operation,
|
|
19662
|
+
maxRetries: opts.maxRetries
|
|
19663
|
+
});
|
|
19664
|
+
} catch (err) {
|
|
19665
|
+
if (!this.putFallbackEligible(err)) throw err;
|
|
19666
|
+
const merged = await this.putFallbackMergedBody(
|
|
19667
|
+
() => this.getScopedUnredacted(parentId, numeric, { maxRetries: opts.maxRetries }),
|
|
19668
|
+
body,
|
|
19669
|
+
numeric
|
|
19670
|
+
);
|
|
19671
|
+
let retry;
|
|
19672
|
+
try {
|
|
19673
|
+
retry = await this.http.request({
|
|
19674
|
+
method: "PUT",
|
|
19675
|
+
path: `${path}/${numeric}`,
|
|
19676
|
+
body: merged,
|
|
19677
|
+
operation,
|
|
19678
|
+
maxRetries: opts.maxRetries
|
|
19679
|
+
});
|
|
19680
|
+
} catch (rerr) {
|
|
19681
|
+
if (this.putFallbackEligible(rerr)) throw this.putFallbackDeclined(rerr, `PUT ${path}/${numeric}`, true);
|
|
19682
|
+
throw rerr;
|
|
19796
19683
|
}
|
|
19797
|
-
|
|
19684
|
+
return unwrapOperation(retry, operation);
|
|
19798
19685
|
}
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19686
|
+
return unwrapOperation(res, operation);
|
|
19687
|
+
}
|
|
19688
|
+
/** Parent-scoped delete: `DELETE .../{parentId}/<Child>/{id}` → `{ id }`
|
|
19689
|
+
* (a blank 2xx synthesizes `{ id }` from the path id — issue #25). */
|
|
19690
|
+
async deleteScopedRaw(parentId, id, opts = {}) {
|
|
19691
|
+
const numeric = parseIdentifier(id, "id");
|
|
19692
|
+
const res = await this.http.request({
|
|
19693
|
+
method: "DELETE",
|
|
19694
|
+
path: this.scopedItemPath(parentId, id),
|
|
19695
|
+
operation: `${this.config.key}.delete`,
|
|
19696
|
+
maxRetries: opts.maxRetries
|
|
19697
|
+
});
|
|
19698
|
+
return res === void 0 ? { id: numeric } : unwrapOperation(res, `${this.config.key}.delete`);
|
|
19699
|
+
}
|
|
19700
|
+
/**
|
|
19701
|
+
* Attachment delete: the vendor returns the deleted attachment MODEL, not
|
|
19702
|
+
* OperationResultModel (G1 envelope.md) — pass it through typed.
|
|
19703
|
+
*/
|
|
19704
|
+
async deleteScopedModel(parentId, id, opts = {}) {
|
|
19705
|
+
const numeric = parseIdentifier(id, "id");
|
|
19706
|
+
const res = await this.http.request({
|
|
19707
|
+
method: "DELETE",
|
|
19708
|
+
path: this.scopedItemPath(parentId, id),
|
|
19709
|
+
operation: `${this.config.key}.delete`,
|
|
19710
|
+
maxRetries: opts.maxRetries
|
|
19711
|
+
});
|
|
19712
|
+
if (res === void 0) return {};
|
|
19713
|
+
if (!isRecord2(res)) {
|
|
19714
|
+
throw new AutotaskError(
|
|
19715
|
+
`Expected the deleted attachment model for ID ${numeric}; got a different shape`,
|
|
19716
|
+
{ code: "INVALID_RESPONSE", category: "server", resourceIds: [numeric], details: { idRefs: [{ entity: this.entityName, id: numeric }] } }
|
|
19717
|
+
);
|
|
19804
19718
|
}
|
|
19719
|
+
return res;
|
|
19805
19720
|
}
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
19818
|
-
section.rows[key] = { name: null, blind: false, capped: true };
|
|
19819
|
-
section.truncated = true;
|
|
19820
|
-
if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
|
|
19821
|
-
contractLobUnread = "capped";
|
|
19822
|
-
}
|
|
19823
|
-
continue;
|
|
19721
|
+
async createIfNotExists(dataOrParentId, dataOrOpts, opts = {}) {
|
|
19722
|
+
const isObject = (v) => isRecord2(v) && !Array.isArray(v);
|
|
19723
|
+
const scoped = this.config.parentScopedPath !== void 0;
|
|
19724
|
+
let parentId;
|
|
19725
|
+
let data;
|
|
19726
|
+
let cinOpts;
|
|
19727
|
+
if (scoped) {
|
|
19728
|
+
const dataIsOptsBag = isObject(dataOrOpts) && ("dedupFields" in dataOrOpts || "dryRun" in dataOrOpts);
|
|
19729
|
+
if (!isIdentifierCandidate(dataOrParentId) || !isObject(dataOrOpts) || dataIsOptsBag) {
|
|
19730
|
+
throw new AutotaskConfigError(
|
|
19731
|
+
`createIfNotExists for parent-scoped resource ${this.config.key} requires (parentId, data[, opts]) \u2014 the global collection has no create; a missing parentId would be a wrong-path request`
|
|
19732
|
+
);
|
|
19824
19733
|
}
|
|
19825
|
-
|
|
19734
|
+
parentId = parseIdentifier(dataOrParentId, "parentId");
|
|
19735
|
+
data = dataOrOpts;
|
|
19736
|
+
cinOpts = opts;
|
|
19737
|
+
} else {
|
|
19738
|
+
if (!isObject(dataOrParentId)) {
|
|
19739
|
+
throw new AutotaskConfigError(
|
|
19740
|
+
`createIfNotExists for top-level resource ${this.config.key} takes (data[, opts]) \u2014 got a non-record first argument`
|
|
19741
|
+
);
|
|
19742
|
+
}
|
|
19743
|
+
data = dataOrParentId;
|
|
19744
|
+
cinOpts = isObject(dataOrOpts) ? dataOrOpts : {};
|
|
19826
19745
|
}
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19746
|
+
const record = isRecord2(data) ? data : {};
|
|
19747
|
+
const dedupFields = (cinOpts.dedupFields ?? []).filter((field) => field in record);
|
|
19748
|
+
let existing;
|
|
19749
|
+
let existingId;
|
|
19750
|
+
if (dedupFields.length > 0) {
|
|
19751
|
+
const conditions = dedupFields.map((field) => ({
|
|
19752
|
+
field,
|
|
19753
|
+
op: "eq",
|
|
19754
|
+
value: record[field]
|
|
19755
|
+
}));
|
|
19756
|
+
if (parentId !== void 0 && this.config.parentField !== void 0) {
|
|
19757
|
+
conditions.unshift({ field: this.config.parentField, op: "eq", value: parentId });
|
|
19831
19758
|
}
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
if (
|
|
19835
|
-
|
|
19836
|
-
|
|
19837
|
-
|
|
19838
|
-
|
|
19839
|
-
contractLobUnread = "unresolved";
|
|
19759
|
+
const matches = await this.queryEntity(`${this.resourcePath}/query`, conditions, { maxRecords: 10 });
|
|
19760
|
+
const first = matches[0];
|
|
19761
|
+
if (first !== void 0) {
|
|
19762
|
+
const firstId = first.id;
|
|
19763
|
+
if (isIdentifierCandidate(firstId)) {
|
|
19764
|
+
existing = first;
|
|
19765
|
+
existingId = parseIdentifier(firstId, "id");
|
|
19840
19766
|
}
|
|
19841
|
-
} else {
|
|
19842
|
-
throw err;
|
|
19843
19767
|
}
|
|
19844
19768
|
}
|
|
19845
|
-
|
|
19846
|
-
|
|
19847
|
-
|
|
19848
|
-
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
const labelResource = plan.labelResource ?? "tickets";
|
|
19852
|
-
const catalog = await fieldCatalog(client, { resources: [labelResource], labels: true, fields: [...plan.labelFields] });
|
|
19853
|
-
const row = catalog.rows.find((r) => r.resource === labelResource);
|
|
19854
|
-
if (row === void 0 || row.error !== null || row.fields === void 0) {
|
|
19855
|
-
usable = false;
|
|
19856
|
-
failure = row?.error ?? { code: "INVALID_RESPONSE", message: `fieldCatalog: no ${labelResource} row` };
|
|
19857
|
-
} else {
|
|
19858
|
-
for (const f of plan.labelFields) {
|
|
19859
|
-
const frow = row.fields.find((x) => x.name === f);
|
|
19860
|
-
picklists[f] = frow?.picklistValues;
|
|
19861
|
-
}
|
|
19769
|
+
if (cinOpts.dryRun === true) {
|
|
19770
|
+
const checks = [
|
|
19771
|
+
{ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 }
|
|
19772
|
+
];
|
|
19773
|
+
if (dedupFields.length > 0) {
|
|
19774
|
+
checks.push({ name: "duplicate-detection", ok: existingId === void 0 });
|
|
19862
19775
|
}
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19776
|
+
const warnings = existingId !== void 0 ? [`duplicate match on ${dedupFields.join(", ")} (id ${existingId}); the create would be skipped`] : [];
|
|
19777
|
+
return buildDryRunResult({
|
|
19778
|
+
operation: `${this.config.key}.createIfNotExists`,
|
|
19779
|
+
method: "POST",
|
|
19780
|
+
path: parentId !== void 0 ? this.scopedCollectionPath(parentId) : this.resourcePath,
|
|
19781
|
+
ids: existingId !== void 0 ? [existingId] : [],
|
|
19782
|
+
checks,
|
|
19783
|
+
warnings,
|
|
19784
|
+
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19785
|
+
data
|
|
19786
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19868
19787
|
}
|
|
19869
|
-
if (
|
|
19870
|
-
|
|
19871
|
-
for (const f of plan.labelFields) section.reasons[f] = PICKLIST_CATALOG_UNAVAILABLE_NOTE;
|
|
19788
|
+
if (existing !== void 0 && existingId !== void 0) {
|
|
19789
|
+
return { id: existingId, created: false, existing };
|
|
19872
19790
|
}
|
|
19791
|
+
const op = parentId !== void 0 ? await this.createScopedRaw(parentId, data) : await this.createGlobal(data);
|
|
19792
|
+
return { id: op.id, created: true };
|
|
19873
19793
|
}
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19889
|
-
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
|
|
19794
|
+
// ------------------------------------------------------------------
|
|
19795
|
+
// Dry-run scaffolding + stale-object guard (mutation safety, section 19.3)
|
|
19796
|
+
// ------------------------------------------------------------------
|
|
19797
|
+
/**
|
|
19798
|
+
* Stale-object guard (section 19.3): compare the plan's `staleCheck` field
|
|
19799
|
+
* (vendor `lastModifiedDate`) against the caller-observed value. Mismatch
|
|
19800
|
+
* throws `STALE_OBJECT`. Returns true when a comparison was actually made
|
|
19801
|
+
* (the resource model carries the field AND the vendor answered it AND the
|
|
19802
|
+
* caller supplied an expected value); false when the check is unavailable.
|
|
19803
|
+
*/
|
|
19804
|
+
staleObjectCheck(current, opts, operation) {
|
|
19805
|
+
const field = this.config.staleField;
|
|
19806
|
+
const expected = opts.lastModifiedDate;
|
|
19807
|
+
if (field === void 0 || expected === void 0) return false;
|
|
19808
|
+
const actual = current[field];
|
|
19809
|
+
if (typeof actual !== "string") return false;
|
|
19810
|
+
if (actual !== expected) {
|
|
19811
|
+
throw new StaleObjectError(
|
|
19812
|
+
`stale object: ${this.config.key} ${field} changed since the caller read it (expected ${expected}, current ${actual})`,
|
|
19813
|
+
{
|
|
19814
|
+
code: "STALE_OBJECT",
|
|
19815
|
+
operation,
|
|
19816
|
+
suggestedAction: "Re-read the record and retry with the fresh lastModifiedDate."
|
|
19817
|
+
}
|
|
19818
|
+
);
|
|
19819
|
+
}
|
|
19820
|
+
return true;
|
|
19895
19821
|
}
|
|
19896
|
-
|
|
19897
|
-
|
|
19898
|
-
|
|
19899
|
-
|
|
19900
|
-
|
|
19901
|
-
|
|
19902
|
-
|
|
19903
|
-
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19822
|
+
/**
|
|
19823
|
+
* Stale-object guard for a REAL mutation: the caller supplied an expected
|
|
19824
|
+
* `lastModifiedDate` and the model carries the field → fetch the current
|
|
19825
|
+
* record and compare before the mutating request (mismatch throws
|
|
19826
|
+
* STALE_OBJECT; a vanished record propagates the vendor's NotFoundError).
|
|
19827
|
+
*/
|
|
19828
|
+
async guardStaleBeforeMutation(id, opts, operation, parentId) {
|
|
19829
|
+
if (opts.lastModifiedDate === void 0 || this.config.staleField === void 0) return;
|
|
19830
|
+
let current;
|
|
19831
|
+
try {
|
|
19832
|
+
current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
|
|
19833
|
+
} catch (err) {
|
|
19834
|
+
if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
|
|
19835
|
+
throw err;
|
|
19836
|
+
}
|
|
19837
|
+
this.staleObjectCheck(current, opts, operation);
|
|
19838
|
+
}
|
|
19839
|
+
/**
|
|
19840
|
+
* `create` dry-run: input validation + parent verification (scoped
|
|
19841
|
+
* resources) + the pinned envelope. NO request is issued (scoped parent
|
|
19842
|
+
* verification is a read). Duplicate detection has no reliable lookup for
|
|
19843
|
+
* a plain create (that is `createIfNotExists`), so it is not claimed.
|
|
19844
|
+
*/
|
|
19845
|
+
async runCreateDryRun(data, parentId) {
|
|
19846
|
+
const checks = [];
|
|
19847
|
+
const warnings = [];
|
|
19848
|
+
checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
|
|
19849
|
+
const deleteBoundaryNote = DELETE_BOUNDARY_NOTES[this.config.key];
|
|
19850
|
+
if (deleteBoundaryNote !== void 0) {
|
|
19851
|
+
checks.push({ name: "delete-boundary", ok: true });
|
|
19852
|
+
warnings.push(deleteBoundaryNote);
|
|
19853
|
+
}
|
|
19854
|
+
let path = this.resourcePath;
|
|
19855
|
+
if (parentId !== void 0) {
|
|
19856
|
+
path = this.scopedCollectionPath(parentId);
|
|
19857
|
+
const parentPath = this.config.parentPath;
|
|
19858
|
+
if (parentPath === void 0) {
|
|
19859
|
+
throw new AutotaskConfigError(
|
|
19860
|
+
`dry-run: ${this.config.key} is parent-scoped but has no parentPath configured`
|
|
19861
|
+
);
|
|
19862
|
+
}
|
|
19863
|
+
let parent = null;
|
|
19864
|
+
if (isIdentifierCandidate(parentId)) {
|
|
19865
|
+
try {
|
|
19866
|
+
parent = await this.fetchRecordRaw(
|
|
19867
|
+
parentPath,
|
|
19868
|
+
parseIdentifier(parentId, "parentId"),
|
|
19869
|
+
true
|
|
19870
|
+
);
|
|
19871
|
+
} catch (err) {
|
|
19872
|
+
if (!(err instanceof NotFoundError)) throw err;
|
|
19873
|
+
}
|
|
19907
19874
|
}
|
|
19908
|
-
|
|
19909
|
-
if (
|
|
19910
|
-
|
|
19911
|
-
return null;
|
|
19875
|
+
checks.push({ name: "parent-exists", ok: parent !== null });
|
|
19876
|
+
if (parent === null) {
|
|
19877
|
+
warnings.push(`parent ${parentId} not found; the vendor would reject the create`);
|
|
19912
19878
|
}
|
|
19913
|
-
return typeof hit.label === "string" ? hit.label : null;
|
|
19914
19879
|
}
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
ticketID: "ticket",
|
|
19921
|
-
roleID: "role",
|
|
19922
|
-
billingCodeID: "billingCode"
|
|
19923
|
-
};
|
|
19924
|
-
var isResourceFkField = (field) => field === "resourceID" || field.endsWith("ResourceID");
|
|
19925
|
-
var LABEL_NAME_MAX_CHARS = 200;
|
|
19926
|
-
function untrustedName(name) {
|
|
19927
|
-
if (name === null) return null;
|
|
19928
|
-
const [[capped]] = truncateNoteChars([{ v: name }], LABEL_NAME_MAX_CHARS, ["v"]);
|
|
19929
|
-
return wrapUntrusted(capped.v) ?? capped.v;
|
|
19930
|
-
}
|
|
19931
|
-
var lastPicklists = /* @__PURE__ */ new WeakMap();
|
|
19932
|
-
function readNamesOpt(value, where) {
|
|
19933
|
-
if (value === void 0) return true;
|
|
19934
|
-
if (typeof value !== "boolean") {
|
|
19935
|
-
throw new AutotaskConfigError(`${where}: names must be a boolean (got ${typeof value}) \u2014 true (default) adds label siblings, false returns raw ids only`);
|
|
19936
|
-
}
|
|
19937
|
-
return value;
|
|
19938
|
-
}
|
|
19939
|
-
async function labelRows(client, resourceKey, rows, resourceKeyedReasons = false) {
|
|
19940
|
-
const keys = /* @__PURE__ */ new Set();
|
|
19941
|
-
for (const row of rows) for (const k of Object.keys(row)) keys.add(k);
|
|
19942
|
-
if (keys.size === 0) return { rows: [...rows] };
|
|
19943
|
-
let picklistFields = [];
|
|
19944
|
-
let unavailable = [];
|
|
19945
|
-
let catalogError;
|
|
19946
|
-
const modelFields = MODEL_FIELD_SETS[resourceKey] ?? [];
|
|
19947
|
-
const candidates = [...keys].filter((f) => f !== "id" && modelFields.includes(f) && ROW_FK_TABLES[f] === void 0 && !isResourceFkField(f));
|
|
19948
|
-
if (candidates.length > 0) try {
|
|
19949
|
-
const catalog = await fieldCatalog(client, { resources: [resourceKey], labels: true });
|
|
19950
|
-
const row = catalog.rows.find((r) => r.resource === resourceKey);
|
|
19951
|
-
const known = lastPicklists.get(client) ?? /* @__PURE__ */ new Map();
|
|
19952
|
-
lastPicklists.set(client, known);
|
|
19953
|
-
if (row?.fields === void 0) {
|
|
19954
|
-
const last = known.get(resourceKey);
|
|
19955
|
-
unavailable = (last ?? []).filter((f) => candidates.includes(f));
|
|
19956
|
-
if (last === void 0 || unavailable.length > 0) {
|
|
19957
|
-
catalogError = row?.error ?? { code: "INVALID_RESPONSE", message: `fieldCatalog: no ${resourceKey} row` };
|
|
19880
|
+
if (isRecord2(data)) {
|
|
19881
|
+
const eff = effectiveRequiredFields(void 0, RELATIONSHIP_MAP[this.config.key]?.zoneFacts ?? null);
|
|
19882
|
+
const missing = eff.fields.filter((f) => f !== this.config.parentField && data[f] === void 0);
|
|
19883
|
+
if (missing.length > 0) {
|
|
19884
|
+
warnings.push(`required field(s) not in the data: ${missing.join(", ")} (${eff.from}); the vendor would likely reject the create`);
|
|
19958
19885
|
}
|
|
19959
|
-
} else {
|
|
19960
|
-
const all = row.fields.filter((f) => f.isPickList).map((f) => f.name);
|
|
19961
|
-
known.set(resourceKey, all);
|
|
19962
|
-
picklistFields = all.filter((f) => candidates.includes(f));
|
|
19963
19886
|
}
|
|
19964
|
-
|
|
19965
|
-
|
|
19966
|
-
}
|
|
19967
|
-
const labelled = (field) => picklistFields.includes(field) || unavailable.includes(field) || ROW_FK_TABLES[field] !== void 0 || isResourceFkField(field);
|
|
19968
|
-
if (catalogError === void 0 && ![...keys].some(labelled)) return { rows: [...rows] };
|
|
19969
|
-
const builder = createNamePlan(picklistFields, resourceKey);
|
|
19970
|
-
for (const row of rows) {
|
|
19971
|
-
for (const [field, value] of Object.entries(row)) {
|
|
19972
|
-
const table = ROW_FK_TABLES[field];
|
|
19973
|
-
if ((value === 0 || value === "0") && !zeroIsRecord(table)) continue;
|
|
19974
|
-
if (table !== void 0) builder.addFk(table, value);
|
|
19975
|
-
else if (isResourceFkField(field)) builder.addResource(value);
|
|
19887
|
+
if (this.config.key === "companies") {
|
|
19888
|
+
warnings.push('the vendor auto-creates a "Primary Address" location for a new company (rename it with company_locations.update; it cannot be deleted via the API)');
|
|
19976
19889
|
}
|
|
19890
|
+
return buildDryRunResult({
|
|
19891
|
+
operation: `${this.config.key}.create`,
|
|
19892
|
+
method: "POST",
|
|
19893
|
+
path,
|
|
19894
|
+
ids: [],
|
|
19895
|
+
checks,
|
|
19896
|
+
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19897
|
+
warnings,
|
|
19898
|
+
data
|
|
19899
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19977
19900
|
}
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
}
|
|
19985
|
-
|
|
19986
|
-
const
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19901
|
+
/**
|
|
19902
|
+
* `update` dry-run: input validation + current-record fetch (a missing
|
|
19903
|
+
* record carries `warnings` and no `diff` — pinned), the stale-object
|
|
19904
|
+
* check (mismatch throws STALE_OBJECT) and the best-effort `diff`. NO
|
|
19905
|
+
* mutating request is issued.
|
|
19906
|
+
*/
|
|
19907
|
+
async runUpdateDryRun(id, data, opts = {}, parentId) {
|
|
19908
|
+
const operation = `${this.config.key}.update`;
|
|
19909
|
+
const checks = [];
|
|
19910
|
+
const warnings = [];
|
|
19911
|
+
checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
|
|
19912
|
+
const scopedUpdate = parentId !== void 0 && this.config.parentScopedPath !== void 0;
|
|
19913
|
+
const collectionUpdate = scopedUpdate || this.config.collectionUpdate === true;
|
|
19914
|
+
const path = scopedUpdate ? this.scopedCollectionPath(parentId) : collectionUpdate ? this.resourcePath : `${this.resourcePath}/${id}`;
|
|
19915
|
+
const method = scopedUpdate ? "PATCH" : "PUT";
|
|
19916
|
+
let current = null;
|
|
19917
|
+
try {
|
|
19918
|
+
current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
|
|
19919
|
+
} catch (err) {
|
|
19920
|
+
if (err instanceof NotFoundError) {
|
|
19921
|
+
const updateReason = EMPTY_REASON_STAMPS[this.config.key]?.update;
|
|
19922
|
+
checks.push({
|
|
19923
|
+
name: "current-record-exists",
|
|
19924
|
+
ok: false,
|
|
19925
|
+
...updateReason !== void 0 ? { note: updateReason.note } : {}
|
|
19926
|
+
});
|
|
19927
|
+
warnings.push(`current record ${id} not fetchable; no diff`);
|
|
19928
|
+
return buildDryRunResult({
|
|
19929
|
+
operation,
|
|
19930
|
+
method,
|
|
19931
|
+
path,
|
|
19932
|
+
ids: [id],
|
|
19933
|
+
checks,
|
|
19934
|
+
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19935
|
+
warnings,
|
|
19936
|
+
data,
|
|
19937
|
+
...updateReason !== void 0 ? { emptyReason: updateReason } : {}
|
|
19938
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19939
|
+
}
|
|
19940
|
+
throw err;
|
|
19995
19941
|
}
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
19942
|
+
checks.push({ name: "current-record-exists", ok: true });
|
|
19943
|
+
if (this.staleObjectCheck(current, opts, operation)) {
|
|
19944
|
+
checks.push({ name: "stale-check", ok: true });
|
|
19945
|
+
}
|
|
19946
|
+
let envelopeData = data;
|
|
19947
|
+
if (parentId === void 0 && this.config.collectionUpdate === true && opts.raw !== true) {
|
|
19948
|
+
envelopeData = mergePartialBody(
|
|
19949
|
+
current,
|
|
19950
|
+
this.normalizeMutationBody(data)
|
|
19951
|
+
);
|
|
19952
|
+
}
|
|
19953
|
+
return buildDryRunResult({
|
|
19954
|
+
operation,
|
|
19955
|
+
method,
|
|
19956
|
+
path,
|
|
19957
|
+
ids: [id],
|
|
19958
|
+
checks,
|
|
19959
|
+
diff: computeFieldDiff(current, data),
|
|
19960
|
+
impact: { affected: 1, scope: "single-record", reversible: true },
|
|
19961
|
+
warnings,
|
|
19962
|
+
data: envelopeData
|
|
19963
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
20011
19964
|
}
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
19965
|
+
/**
|
|
19966
|
+
* `delete` dry-run: current-record verification + the stale-object check +
|
|
19967
|
+
* the pinned envelope (no diff — a delete has no before/after fields). NO
|
|
19968
|
+
* mutating request is issued.
|
|
19969
|
+
*/
|
|
19970
|
+
async runDeleteDryRun(id, opts = {}, parentId) {
|
|
19971
|
+
const operation = `${this.config.key}.delete`;
|
|
19972
|
+
const checks = [];
|
|
19973
|
+
const warnings = [];
|
|
19974
|
+
const path = parentId === void 0 ? `${this.resourcePath}/${id}` : this.scopedItemPath(parentId, id);
|
|
19975
|
+
let current = null;
|
|
19976
|
+
try {
|
|
19977
|
+
current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
|
|
19978
|
+
} catch (err) {
|
|
19979
|
+
if (err instanceof NotFoundError) {
|
|
19980
|
+
checks.push({ name: "current-record-exists", ok: false });
|
|
19981
|
+
warnings.push(`record ${id} not fetchable; the vendor would reject the delete`);
|
|
19982
|
+
return buildDryRunResult({
|
|
19983
|
+
operation,
|
|
19984
|
+
method: "DELETE",
|
|
19985
|
+
path,
|
|
19986
|
+
ids: [id],
|
|
19987
|
+
checks,
|
|
19988
|
+
impact: { affected: 1, scope: "single-record", reversible: false },
|
|
19989
|
+
warnings
|
|
19990
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
19991
|
+
}
|
|
19992
|
+
throw err;
|
|
20020
19993
|
}
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
if (labelled.names.catalogError !== void 0) catalogErrors[resource] = labelled.names.catalogError;
|
|
20041
|
-
}
|
|
20042
|
-
}
|
|
20043
|
-
if (summaries.length === 0) return { ...result, results };
|
|
20044
|
-
const catalogError = Object.values(catalogErrors)[0];
|
|
20045
|
-
const names = {
|
|
20046
|
-
truncated: summaries.some((s) => s.truncated),
|
|
20047
|
-
capped: summaries.reduce((n, s) => n + s.capped, 0),
|
|
20048
|
-
note: summaries.map((s) => s.note).join("; "),
|
|
20049
|
-
...Object.keys(reasons).length > 0 ? { reasons } : {},
|
|
20050
|
-
...catalogError !== void 0 ? { catalogError, catalogErrors } : {}
|
|
20051
|
-
};
|
|
20052
|
-
return { ...result, results, names };
|
|
20053
|
-
}
|
|
19994
|
+
checks.push({ name: "current-record-exists", ok: true });
|
|
19995
|
+
if (this.staleObjectCheck(current, opts, operation)) {
|
|
19996
|
+
checks.push({ name: "stale-check", ok: true });
|
|
19997
|
+
}
|
|
19998
|
+
return buildDryRunResult({
|
|
19999
|
+
operation,
|
|
20000
|
+
method: "DELETE",
|
|
20001
|
+
path,
|
|
20002
|
+
ids: [id],
|
|
20003
|
+
checks,
|
|
20004
|
+
impact: { affected: 1, scope: "single-record", reversible: false },
|
|
20005
|
+
warnings
|
|
20006
|
+
}, { redactPersonalData: this.http.redactPersonalData });
|
|
20007
|
+
}
|
|
20008
|
+
};
|
|
20009
|
+
|
|
20010
|
+
// src/resources/base/BaseResource.ts
|
|
20011
|
+
var BaseResource = class extends MutationBase {
|
|
20012
|
+
};
|
|
20054
20013
|
|
|
20055
20014
|
// src/resources/billing_codes.ts
|
|
20056
20015
|
var BillingCodesResource = class extends BaseResource {
|
|
@@ -20954,7 +20913,7 @@ function formatUnknownNameList(names, cap = UNKNOWN_NAME_LIST_CAP) {
|
|
|
20954
20913
|
const more = names.length > cap ? ` (and ${names.length - cap} more)` : "";
|
|
20955
20914
|
return `${listed}${more}`;
|
|
20956
20915
|
}
|
|
20957
|
-
function collectFilterFieldNames(conditions) {
|
|
20916
|
+
function collectFilterFieldNames(conditions, resourceKey) {
|
|
20958
20917
|
const out = [];
|
|
20959
20918
|
const walk = (conds) => {
|
|
20960
20919
|
for (const cond of conds) {
|
|
@@ -20964,7 +20923,16 @@ function collectFilterFieldNames(conditions) {
|
|
|
20964
20923
|
continue;
|
|
20965
20924
|
}
|
|
20966
20925
|
if (cond.udf === true) continue;
|
|
20967
|
-
if (typeof cond.field === "string" && cond.field.length > 0
|
|
20926
|
+
if (typeof cond.field === "string" && cond.field.length > 0) {
|
|
20927
|
+
if (!cond.field.includes(".")) {
|
|
20928
|
+
out.push(cond.field);
|
|
20929
|
+
continue;
|
|
20930
|
+
}
|
|
20931
|
+
if (resourceKey !== void 0) {
|
|
20932
|
+
const parts = cond.field.split(".");
|
|
20933
|
+
if (parts.length === 2 && selfPrefixes(resourceKey).includes(parts[0])) out.push(parts[1]);
|
|
20934
|
+
}
|
|
20935
|
+
}
|
|
20968
20936
|
}
|
|
20969
20937
|
};
|
|
20970
20938
|
walk(conditions);
|
|
@@ -21027,10 +20995,17 @@ function unknownNamesRefusal(record, authority, known, unknownInclude, unknownFi
|
|
|
21027
20995
|
message,
|
|
21028
20996
|
{
|
|
21029
20997
|
operation: record.id,
|
|
21030
|
-
suggestedAction:
|
|
20998
|
+
suggestedAction: unknownNamesSuggestedAction(record, authority, unknownFilter.length > 0)
|
|
21031
20999
|
}
|
|
21032
21000
|
);
|
|
21033
21001
|
}
|
|
21002
|
+
function unknownNamesSuggestedAction(record, authority, filterUnknown) {
|
|
21003
|
+
const vendor = `Use exact vendor field names (operations.fieldCatalog resources: ["${record.resource}"] lists the live set).`;
|
|
21004
|
+
if (filterUnknown && record.udfCapable) {
|
|
21005
|
+
return `${vendor} UDF names: filter with udf: true (max 1 per query); names + picklist ids via fieldCatalog resources: ["${record.resource}"], include: ["udf"], labels: true; fieldCheck: false = vendor names only (never UDF names). Authority: ${authority}.`;
|
|
21006
|
+
}
|
|
21007
|
+
return `${vendor} Pass fieldCheck: false to disable this name check. The authority here was ${authority}.`;
|
|
21008
|
+
}
|
|
21034
21009
|
async function validateQueryFieldNames(record, holder, args) {
|
|
21035
21010
|
const bag = args[0];
|
|
21036
21011
|
if (!isRecord2(bag)) return;
|
|
@@ -21039,7 +21014,7 @@ async function validateQueryFieldNames(record, holder, args) {
|
|
|
21039
21014
|
let filterNames;
|
|
21040
21015
|
if (bag["filter"] !== void 0) {
|
|
21041
21016
|
const conditions = normalizedConditions(bag["filter"]);
|
|
21042
|
-
filterNames = collectFilterFieldNames(conditions ?? []);
|
|
21017
|
+
filterNames = collectFilterFieldNames(conditions ?? [], record.resource);
|
|
21043
21018
|
}
|
|
21044
21019
|
if (fieldCheck === false) return;
|
|
21045
21020
|
const found = await findUnknownFieldNames(record, holder, { includeFields: includeNames ?? [], filter: filterNames ?? [] });
|
|
@@ -21653,7 +21628,22 @@ async function readBillingPart(http, v, companyIds) {
|
|
|
21653
21628
|
if (v.dateFrom !== void 0) billingConditions.push({ field: "itemDate", op: "gte", value: v.dateFrom });
|
|
21654
21629
|
if (v.dateTo !== void 0) billingConditions.push({ field: "itemDate", op: "lte", value: v.dateTo });
|
|
21655
21630
|
}
|
|
21656
|
-
const billingFields =
|
|
21631
|
+
const billingFields = [
|
|
21632
|
+
"companyID",
|
|
21633
|
+
"ticketID",
|
|
21634
|
+
"projectID",
|
|
21635
|
+
"timeEntryID",
|
|
21636
|
+
"contractServiceID",
|
|
21637
|
+
"contractID",
|
|
21638
|
+
"billingCodeID",
|
|
21639
|
+
"expenseItemID",
|
|
21640
|
+
"itemDate",
|
|
21641
|
+
"quantity",
|
|
21642
|
+
"rate",
|
|
21643
|
+
"extendedPrice",
|
|
21644
|
+
"totalAmount",
|
|
21645
|
+
"nonBillable"
|
|
21646
|
+
];
|
|
21657
21647
|
const billingIncludeFields = [...billingFields, "id", ...v.includeNotes ? ["description"] : []];
|
|
21658
21648
|
const billingRaw = await boundedPart(http, "/V1.0/BillingItems/query", billingConditions, v.limit, OP3, billingIncludeFields);
|
|
21659
21649
|
let billingItems = billingRaw.items;
|
|
@@ -22038,7 +22028,7 @@ async function fetchMany(client, items, opts = {}) {
|
|
|
22038
22028
|
if (opts.liveFields !== void 0 && typeof opts.liveFields !== "boolean") {
|
|
22039
22029
|
throw new AutotaskConfigError(`fetchMany: liveFields must be a boolean (got ${typeof opts.liveFields})`);
|
|
22040
22030
|
}
|
|
22041
|
-
const labels = readNamesOpt(opts.names, "fetchMany");
|
|
22031
|
+
const labels = readNamesOpt(client, opts.names, "fetchMany");
|
|
22042
22032
|
const cap = Math.min(FETCH_MANY_MAX_ITEMS, opts.maxItems ?? FETCH_MANY_MAX_ITEMS);
|
|
22043
22033
|
if (opts.liveFields === true) {
|
|
22044
22034
|
validateFetchManyItems(items, cap, void 0, void 0, { fieldCheck: false });
|
|
@@ -22123,7 +22113,7 @@ async function fetchByIds(client, items, opts = {}) {
|
|
|
22123
22113
|
throw new AutotaskConfigError(`fetchByIds: maxItems must be an integer >= 1 (got ${String(opts.maxItems)})`);
|
|
22124
22114
|
}
|
|
22125
22115
|
}
|
|
22126
|
-
const labels = readNamesOpt(opts.names, "fetchByIds");
|
|
22116
|
+
const labels = readNamesOpt(client, opts.names, "fetchByIds");
|
|
22127
22117
|
const cap = Math.min(FETCH_BY_IDS_MAX_ITEMS, opts.maxItems ?? FETCH_BY_IDS_MAX_ITEMS);
|
|
22128
22118
|
validateFetchManyItems(items, cap, "fetchByIds", `FETCH_BY_IDS_MAX_ITEMS = ${FETCH_BY_IDS_MAX_ITEMS}`);
|
|
22129
22119
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -22729,7 +22719,7 @@ function appendWiresNote(wires, note) {
|
|
|
22729
22719
|
wires.note = (wires.note ? wires.note + " | " : "") + note;
|
|
22730
22720
|
}
|
|
22731
22721
|
function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
22732
|
-
const
|
|
22722
|
+
const OP8 = PROFITABILITY_OP;
|
|
22733
22723
|
const id = parseIdentifier(contractId, "contractId");
|
|
22734
22724
|
const idNum = typeof id === "number" ? id : Number(id);
|
|
22735
22725
|
const wires = { issued: 0, requests: 0, budget };
|
|
@@ -22741,19 +22731,19 @@ function makeFetchCtx(rawHttp, v, contractId, budget) {
|
|
|
22741
22731
|
} };
|
|
22742
22732
|
const readPart = (path, conditions, fields) => {
|
|
22743
22733
|
wires.issued += 1;
|
|
22744
|
-
return degradable(() => boundedPart(http, path, conditions, v.limit,
|
|
22734
|
+
return degradable(() => boundedPart(http, path, conditions, v.limit, OP8, fields));
|
|
22745
22735
|
};
|
|
22746
22736
|
const truncatedMonths = {};
|
|
22747
22737
|
const windowedPart = async (name, key, path, cond, fields, dateField) => {
|
|
22748
22738
|
const first = await readPart(path, cond, fields);
|
|
22749
22739
|
if (first.absent !== void 0 || !first.hasMore) return { part: stampPart(key, first, cond), rows: first.items };
|
|
22750
|
-
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit,
|
|
22740
|
+
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit, OP8, fields, dateField, v.dateFrom, v.dateTo, wires));
|
|
22751
22741
|
if (!("truncatedMonths" in wp)) return { part: stampPart(key, wp, cond), rows: [] };
|
|
22752
22742
|
const { truncatedMonths: tm, ...part } = wp;
|
|
22753
22743
|
truncatedMonths[name] = tm;
|
|
22754
22744
|
return { part, rows: wp.items };
|
|
22755
22745
|
};
|
|
22756
|
-
return { v, OP:
|
|
22746
|
+
return { v, OP: OP8, id, idNum, rawHttp, http, wires, windowOn, readPart, windowedPart, truncatedMonths };
|
|
22757
22747
|
}
|
|
22758
22748
|
|
|
22759
22749
|
// src/resources/special/contracts/profitability.stages.ts
|
|
@@ -24385,50 +24375,50 @@ async function queryTimeEntries(_resource, filter, limit, operation, http, curso
|
|
|
24385
24375
|
const { items, pageDetails } = unwrapItems(res);
|
|
24386
24376
|
return { items, hasMore: pageDetails.nextPageUrl != null, nextCursor: pageDetails.nextPageUrl ?? null };
|
|
24387
24377
|
}
|
|
24388
|
-
function pagingOpts(opts,
|
|
24378
|
+
function pagingOpts(opts, OP8) {
|
|
24389
24379
|
if (opts?.countOnly !== void 0 && typeof opts.countOnly !== "boolean") {
|
|
24390
|
-
throw new AutotaskConfigError(`${
|
|
24380
|
+
throw new AutotaskConfigError(`${OP8}: countOnly must be a boolean`, { operation: OP8 });
|
|
24391
24381
|
}
|
|
24392
24382
|
const countOnly = opts?.countOnly === true;
|
|
24393
24383
|
const cursor = opts?.cursor;
|
|
24394
24384
|
if (cursor !== void 0 && (typeof cursor !== "string" || cursor.trim() === "")) {
|
|
24395
|
-
throw new AutotaskConfigError(`${
|
|
24385
|
+
throw new AutotaskConfigError(`${OP8}: cursor must be the non-empty nextCursor of a previous call`, { operation: OP8 });
|
|
24396
24386
|
}
|
|
24397
24387
|
if (cursor !== void 0 && cursor.length > CURSOR_MAX) {
|
|
24398
|
-
throw new ResponseRejectedError(`${
|
|
24388
|
+
throw new ResponseRejectedError(`${OP8}: refused the pagination cursor: ${cursor.length} chars is over the ${CURSOR_MAX}-char cap (a vendor nextPageUrl is far shorter)`);
|
|
24399
24389
|
}
|
|
24400
24390
|
if (cursor !== void 0 && countOnly) {
|
|
24401
|
-
throw new AutotaskConfigError(`${
|
|
24391
|
+
throw new AutotaskConfigError(`${OP8}: cursor does not apply with countOnly (countOnly reads every page itself)`, { operation: OP8 });
|
|
24402
24392
|
}
|
|
24403
24393
|
const budget = opts?.maxVendorCalls ?? COUNT_BUDGET_DEFAULT;
|
|
24404
24394
|
if (!Number.isInteger(budget) || budget < 1 || budget > COUNT_BUDGET_MAX) {
|
|
24405
|
-
throw new AutotaskConfigError(`${
|
|
24395
|
+
throw new AutotaskConfigError(`${OP8}: maxVendorCalls must be an integer 1-${COUNT_BUDGET_MAX} (got ${String(opts?.maxVendorCalls)})`, { operation: OP8 });
|
|
24406
24396
|
}
|
|
24407
24397
|
return { ...cursor === void 0 ? {} : { cursor }, countOnly, budget };
|
|
24408
24398
|
}
|
|
24409
|
-
function budgetExceeded(
|
|
24399
|
+
function budgetExceeded(OP8, details, midWalk) {
|
|
24410
24400
|
const { estimate, budget, total } = details;
|
|
24411
24401
|
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}` : "";
|
|
24412
24402
|
return new AutotaskError(
|
|
24413
|
-
midWalk ? `${
|
|
24414
|
-
{ code: "BUDGET_EXCEEDED", category: "policy", operation:
|
|
24403
|
+
midWalk ? `${OP8}: 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` : `${OP8}: 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`,
|
|
24404
|
+
{ code: "BUDGET_EXCEEDED", category: "policy", operation: OP8, details, suggestedAction: `Narrow the dateWorked window (dateFrom/dateTo)${raise}.` }
|
|
24415
24405
|
);
|
|
24416
24406
|
}
|
|
24417
|
-
async function countPostedState(http, filter,
|
|
24407
|
+
async function countPostedState(http, filter, OP8, budget, window) {
|
|
24418
24408
|
let issued = 0;
|
|
24419
24409
|
let estimate = 0;
|
|
24420
24410
|
let counted = 0;
|
|
24421
24411
|
const spend = () => {
|
|
24422
|
-
if (issued >= budget) throw budgetExceeded(
|
|
24412
|
+
if (issued >= budget) throw budgetExceeded(OP8, { estimate, budget, total: counted, issued }, true);
|
|
24423
24413
|
issued += 1;
|
|
24424
24414
|
};
|
|
24425
24415
|
spend();
|
|
24426
24416
|
counted = unwrapQueryCount(
|
|
24427
|
-
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation:
|
|
24428
|
-
|
|
24417
|
+
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation: OP8 }),
|
|
24418
|
+
OP8
|
|
24429
24419
|
);
|
|
24430
24420
|
estimate = 1 + Math.max(1, Math.ceil(counted / PAGE)) + Math.ceil(counted / 250) + 1;
|
|
24431
|
-
if (estimate > budget) throw budgetExceeded(
|
|
24421
|
+
if (estimate > budget) throw budgetExceeded(OP8, { estimate, budget, total: counted, issued }, false);
|
|
24432
24422
|
const states = /* @__PURE__ */ new Map();
|
|
24433
24423
|
let cursor = null;
|
|
24434
24424
|
do {
|
|
@@ -24438,15 +24428,15 @@ async function countPostedState(http, filter, OP9, budget, window) {
|
|
|
24438
24428
|
idempotent: true,
|
|
24439
24429
|
path: cursor === null ? "/V1.0/TimeEntries/query" : entryCursorPath(http.baseUrl, cursor),
|
|
24440
24430
|
body: { MaxRecords: PAGE, IncludeFields: ["id"], filter },
|
|
24441
|
-
operation:
|
|
24431
|
+
operation: OP8
|
|
24442
24432
|
});
|
|
24443
24433
|
const page = unwrapItems(res);
|
|
24444
|
-
const joined = await postedStateOf(http, page.items,
|
|
24434
|
+
const joined = await postedStateOf(http, page.items, OP8, spend);
|
|
24445
24435
|
if (joined.incomplete) {
|
|
24446
|
-
throw new AutotaskError(`${
|
|
24436
|
+
throw new AutotaskError(`${OP8}: countOnly refused: a billing-item join page was truncated (more than 500 billing items for 250 entries), so the posted split cannot be confirmed`, {
|
|
24447
24437
|
code: "RESOLUTION_TRUNCATED",
|
|
24448
24438
|
category: "resolution",
|
|
24449
|
-
operation:
|
|
24439
|
+
operation: OP8,
|
|
24450
24440
|
details: { issued, budget },
|
|
24451
24441
|
suggestedAction: "Narrow the dateWorked window, or page it with cursor (rows mode reports unconfirmed entries via hasMore)."
|
|
24452
24442
|
});
|
|
@@ -24456,14 +24446,14 @@ async function countPostedState(http, filter, OP9, budget, window) {
|
|
|
24456
24446
|
} while (cursor !== null);
|
|
24457
24447
|
spend();
|
|
24458
24448
|
const recounted = unwrapQueryCount(
|
|
24459
|
-
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation:
|
|
24460
|
-
|
|
24449
|
+
await http.request({ method: "POST", idempotent: true, path: "/V1.0/TimeEntries/query/count", body: { filter }, operation: OP8 }),
|
|
24450
|
+
OP8
|
|
24461
24451
|
);
|
|
24462
24452
|
if (states.size !== counted && states.size !== recounted) {
|
|
24463
|
-
throw new AutotaskError(`${
|
|
24453
|
+
throw new AutotaskError(`${OP8}: 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`, {
|
|
24464
24454
|
code: "INVALID_RESPONSE",
|
|
24465
24455
|
category: "server",
|
|
24466
|
-
operation:
|
|
24456
|
+
operation: OP8,
|
|
24467
24457
|
details: { reason: "count_mismatch", streamed: states.size, counted, recounted, issued, budget },
|
|
24468
24458
|
suggestedAction: "Retry the call; a window that changes during the walk can fail or pass depending on timing."
|
|
24469
24459
|
});
|
|
@@ -24520,13 +24510,13 @@ async function postedStateQuery(_resource, mode, http, opts) {
|
|
|
24520
24510
|
return { items, hasMore: read.hasMore || incomplete, window, nextCursor: read.nextCursor, unconfirmed, approvedCount, unapprovedCount: items.length - approvedCount };
|
|
24521
24511
|
}
|
|
24522
24512
|
async function postedWithin(_resource, from, to, http, opts, opLabel = "time_entries.postedWithin") {
|
|
24523
|
-
const
|
|
24524
|
-
const paging = pagingOpts(opts,
|
|
24525
|
-
if (opts?.tz !== void 0) validateZone(opts.tz,
|
|
24526
|
-
const fromB = customRangeDay(from, "from", void 0,
|
|
24527
|
-
const toB = customRangeDay(to, "to", void 0,
|
|
24513
|
+
const OP8 = opLabel;
|
|
24514
|
+
const paging = pagingOpts(opts, OP8);
|
|
24515
|
+
if (opts?.tz !== void 0) validateZone(opts.tz, OP8);
|
|
24516
|
+
const fromB = customRangeDay(from, "from", void 0, OP8);
|
|
24517
|
+
const toB = customRangeDay(to, "to", void 0, OP8);
|
|
24528
24518
|
if (fromB.ms > toB.ms) {
|
|
24529
|
-
throw new AutotaskConfigError(`${
|
|
24519
|
+
throw new AutotaskConfigError(`${OP8}: from (${iso2(fromB.ms)}) must not be after to (${iso2(toB.ms)})`, { operation: OP8 });
|
|
24530
24520
|
}
|
|
24531
24521
|
const windowToMs = toB.ms + DAY_MS2 - 1;
|
|
24532
24522
|
const filter = [
|
|
@@ -24536,9 +24526,9 @@ async function postedWithin(_resource, from, to, http, opts, opLabel = "time_ent
|
|
|
24536
24526
|
if (opts?.ticketID !== void 0) filter.push({ field: "ticketID", op: "eq", value: opts.ticketID });
|
|
24537
24527
|
if (opts?.resourceID !== void 0) filter.push({ field: "resourceID", op: "eq", value: opts.resourceID });
|
|
24538
24528
|
const window = { from: iso2(fromB.ms), to: iso2(windowToMs) };
|
|
24539
|
-
if (paging.countOnly) return countPostedState(http, filter,
|
|
24540
|
-
const { items, hasMore, nextCursor } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit),
|
|
24541
|
-
const { states, incomplete } = await postedStateOf(http, items,
|
|
24529
|
+
if (paging.countOnly) return countPostedState(http, filter, OP8, paging.budget, window);
|
|
24530
|
+
const { items, hasMore, nextCursor } = await queryTimeEntries(_resource, filter, clampWindowLimit(opts?.limit), OP8, http, paging.cursor);
|
|
24531
|
+
const { states, incomplete } = await postedStateOf(http, items, OP8);
|
|
24542
24532
|
let postedCount = 0;
|
|
24543
24533
|
let unpostedCount = 0;
|
|
24544
24534
|
for (const state of states.values()) {
|
|
@@ -25308,103 +25298,6 @@ async function approve(resource, id, opts = {}, http) {
|
|
|
25308
25298
|
};
|
|
25309
25299
|
}
|
|
25310
25300
|
|
|
25311
|
-
// src/resources/special/time_entries/recentTickets.ts
|
|
25312
|
-
var OP8 = "time_entries.recentTickets";
|
|
25313
|
-
var DAY_MS3 = 864e5;
|
|
25314
|
-
var TICKET_CHUNK = 100;
|
|
25315
|
-
var TICKET_FIELDS2 = ["id", "ticketNumber", "title", "status", "assignedResourceID", "companyID"];
|
|
25316
|
-
function positiveId2(value, name) {
|
|
25317
|
-
const n = typeof value === "string" && /^\d+$/.test(value.trim()) ? Number(value) : value;
|
|
25318
|
-
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 1) {
|
|
25319
|
-
throw new AutotaskConfigError(`${OP8}: ${name} must be a positive integer id (got ${String(value)})`, { operation: OP8 });
|
|
25320
|
-
}
|
|
25321
|
-
return n;
|
|
25322
|
-
}
|
|
25323
|
-
async function recentTickets(_resource, from, to, http, opts) {
|
|
25324
|
-
if (opts === void 0 || opts === null || typeof opts !== "object") {
|
|
25325
|
-
throw new AutotaskConfigError(`${OP8}: opts { resourceID, companyID?, tz?, limit? } is required`, { operation: OP8 });
|
|
25326
|
-
}
|
|
25327
|
-
const resourceID = positiveId2(opts.resourceID, "resourceID");
|
|
25328
|
-
const companyID = opts.companyID === void 0 ? void 0 : positiveId2(opts.companyID, "companyID");
|
|
25329
|
-
if (opts.tz !== void 0) validateZone(opts.tz, OP8);
|
|
25330
|
-
const fromB = customRangeDay(from, "from", void 0, OP8);
|
|
25331
|
-
const toB = customRangeDay(to, "to", void 0, OP8);
|
|
25332
|
-
if (fromB.ms > toB.ms) {
|
|
25333
|
-
throw new AutotaskConfigError(`${OP8}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`, { operation: OP8 });
|
|
25334
|
-
}
|
|
25335
|
-
const windowToMs = toB.ms + DAY_MS3 - 1;
|
|
25336
|
-
const { items: entries, hasMore } = await queryTimeEntries(
|
|
25337
|
-
_resource,
|
|
25338
|
-
[
|
|
25339
|
-
{ field: "dateWorked", op: "gte", value: isoUtc(fromB.ms) },
|
|
25340
|
-
{ field: "dateWorked", op: "lte", value: isoUtc(windowToMs) },
|
|
25341
|
-
{ field: "resourceID", op: "eq", value: resourceID }
|
|
25342
|
-
],
|
|
25343
|
-
clampWindowLimit(opts.limit),
|
|
25344
|
-
OP8,
|
|
25345
|
-
http
|
|
25346
|
-
);
|
|
25347
|
-
const groups = /* @__PURE__ */ new Map();
|
|
25348
|
-
let entriesWithoutTicket = 0;
|
|
25349
|
-
for (const e of entries) {
|
|
25350
|
-
if (typeof e.ticketID !== "number") {
|
|
25351
|
-
entriesWithoutTicket += 1;
|
|
25352
|
-
continue;
|
|
25353
|
-
}
|
|
25354
|
-
const g = groups.get(e.ticketID) ?? { entryCount: 0, totalHours: 0, lastWorked: "" };
|
|
25355
|
-
g.entryCount += 1;
|
|
25356
|
-
g.totalHours += e.hoursWorked ?? 0;
|
|
25357
|
-
if ((e.dateWorked ?? "") > g.lastWorked) g.lastWorked = e.dateWorked ?? "";
|
|
25358
|
-
groups.set(e.ticketID, g);
|
|
25359
|
-
}
|
|
25360
|
-
const ids2 = [...groups.keys()];
|
|
25361
|
-
const tickets = /* @__PURE__ */ new Map();
|
|
25362
|
-
for (let c = 0; c < ids2.length; c += TICKET_CHUNK) {
|
|
25363
|
-
const chunk = ids2.slice(c, c + TICKET_CHUNK);
|
|
25364
|
-
const filter = [{ field: "id", op: "in", value: chunk }];
|
|
25365
|
-
const res = await http.request({
|
|
25366
|
-
method: "POST",
|
|
25367
|
-
idempotent: true,
|
|
25368
|
-
path: "/V1.0/Tickets/query",
|
|
25369
|
-
body: { MaxRecords: TICKET_CHUNK, filter, IncludeFields: TICKET_FIELDS2 },
|
|
25370
|
-
operation: OP8
|
|
25371
|
-
});
|
|
25372
|
-
for (const t of unwrapItems(res, OP8).items) if (typeof t.id === "number") tickets.set(t.id, t);
|
|
25373
|
-
}
|
|
25374
|
-
const items = [];
|
|
25375
|
-
const unresolvedTicketIDs = [];
|
|
25376
|
-
let excludedTickets = 0;
|
|
25377
|
-
for (const [ticketID, g] of groups) {
|
|
25378
|
-
const t = tickets.get(ticketID);
|
|
25379
|
-
if (t === void 0) unresolvedTicketIDs.push(ticketID);
|
|
25380
|
-
if (companyID !== void 0 && t?.companyID !== companyID) {
|
|
25381
|
-
if (t !== void 0) excludedTickets += 1;
|
|
25382
|
-
continue;
|
|
25383
|
-
}
|
|
25384
|
-
items.push({
|
|
25385
|
-
ticketID,
|
|
25386
|
-
...t?.ticketNumber !== void 0 ? { ticketNumber: t.ticketNumber } : {},
|
|
25387
|
-
...t?.title !== void 0 ? { title: t.title } : {},
|
|
25388
|
-
...t?.status !== void 0 ? { status: t.status } : {},
|
|
25389
|
-
...t?.assignedResourceID !== void 0 ? { assignedResourceID: t.assignedResourceID } : {},
|
|
25390
|
-
...t?.companyID !== void 0 ? { companyID: t.companyID } : {},
|
|
25391
|
-
entryCount: g.entryCount,
|
|
25392
|
-
totalHours: Math.round(g.totalHours * 100) / 100,
|
|
25393
|
-
lastWorked: g.lastWorked
|
|
25394
|
-
});
|
|
25395
|
-
}
|
|
25396
|
-
items.sort((a, b) => a.lastWorked < b.lastWorked ? 1 : a.lastWorked > b.lastWorked ? -1 : a.ticketID - b.ticketID);
|
|
25397
|
-
return {
|
|
25398
|
-
items,
|
|
25399
|
-
entriesRead: entries.length,
|
|
25400
|
-
entriesWithoutTicket,
|
|
25401
|
-
unresolvedTicketIDs,
|
|
25402
|
-
...companyID !== void 0 ? { excludedTickets } : {},
|
|
25403
|
-
window: { from: isoUtc(fromB.ms), to: isoUtc(windowToMs) },
|
|
25404
|
-
truncated: hasMore
|
|
25405
|
-
};
|
|
25406
|
-
}
|
|
25407
|
-
|
|
25408
25301
|
// src/resources/time_entries.ts
|
|
25409
25302
|
var TimeEntriesResource = class extends BaseResource {
|
|
25410
25303
|
constructor(http, pageSize) {
|
|
@@ -25536,11 +25429,6 @@ var TimeEntriesResource = class extends BaseResource {
|
|
|
25536
25429
|
postedWithin(from, to, opts) {
|
|
25537
25430
|
return postedWithin(this, from, to, this.http, opts);
|
|
25538
25431
|
}
|
|
25539
|
-
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts).
|
|
25540
|
-
* @deprecated Since 0.38.0, removed in the next minor. Use operations.aggregate(client, 'time_entries.query', [{ field: 'resourceID', op: 'eq', value: R }, { field: 'dateWorked', op: 'gte', value: FROM }, { field: 'dateWorked', op: 'lte', value: TO }], { groupBy: ['ticketID'], sum: ['hoursWorked'] }) — group keys carry ticketIDName; add { field: 'ticket.companyID', op: 'eq', value: C } to scope by company. */
|
|
25541
|
-
recentTickets(from, to, opts) {
|
|
25542
|
-
return recentTickets(this, from, to, this.http, opts);
|
|
25543
|
-
}
|
|
25544
25432
|
/** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
|
|
25545
25433
|
today(opts) {
|
|
25546
25434
|
return windowQuery3(this, "today", this.http, opts);
|
|
@@ -25599,6 +25487,8 @@ export {
|
|
|
25599
25487
|
MarkerUnsafeIdError,
|
|
25600
25488
|
RecoveryTruncatedError,
|
|
25601
25489
|
MoveRefusedError,
|
|
25490
|
+
UNSCOPED_WALK_THRESHOLD,
|
|
25491
|
+
ScopeRequiredError,
|
|
25602
25492
|
READ_CACHE_MAX_ENTRIES,
|
|
25603
25493
|
REFERENCE_TTL_MS,
|
|
25604
25494
|
REFERENCE_ENTITIES,
|
|
@@ -25675,6 +25565,7 @@ export {
|
|
|
25675
25565
|
unwrapOperation,
|
|
25676
25566
|
passthrough,
|
|
25677
25567
|
HttpClient,
|
|
25568
|
+
BOUNDED_QUERY_DEFAULT_LIMIT,
|
|
25678
25569
|
BOUNDED_QUERY_MAX_LIMIT,
|
|
25679
25570
|
buildDryRunResult,
|
|
25680
25571
|
mergePartialBody,
|
|
@@ -25686,31 +25577,25 @@ export {
|
|
|
25686
25577
|
paginateItems,
|
|
25687
25578
|
collectAll,
|
|
25688
25579
|
toArray,
|
|
25689
|
-
FIELD_PRESETS,
|
|
25690
|
-
FIELD_PRESET_NAMES,
|
|
25691
|
-
resolveFieldsPreset,
|
|
25692
25580
|
MODEL_DATE_FIELDS,
|
|
25693
|
-
EXPAND_RESOURCE_NAMES_MAX_IDS,
|
|
25694
|
-
expandResourceNames,
|
|
25695
|
-
ENTITY_TYPE_TO_RESOURCE,
|
|
25696
|
-
resourceForEntityType,
|
|
25697
|
-
isEnumerableResource,
|
|
25698
25581
|
WORKFLOW_RESOURCES,
|
|
25699
25582
|
MAX_FIELD_CATALOG_WIRE,
|
|
25700
25583
|
FIELD_CATALOG_HONESTY_NOTE,
|
|
25701
25584
|
MIN_FIELD_CATALOG_MAX_BYTES,
|
|
25702
25585
|
fieldCatalog,
|
|
25586
|
+
EXPAND_RESOURCE_NAMES_MAX_IDS,
|
|
25587
|
+
expandResourceNames,
|
|
25588
|
+
vendorId,
|
|
25589
|
+
resolveLineOfBusinessChain,
|
|
25703
25590
|
PICKLIST_VALUE_NOT_SERVED_NOTE,
|
|
25704
|
-
PICKLIST_CATALOG_UNAVAILABLE_NOTE,
|
|
25705
25591
|
TICKET_LABEL_FIELDS,
|
|
25706
25592
|
createNamePlan,
|
|
25707
25593
|
addNameSibling,
|
|
25708
25594
|
addLabelSibling,
|
|
25709
|
-
vendorId,
|
|
25710
|
-
resolveLineOfBusinessChain,
|
|
25711
25595
|
resolveCompositeNames,
|
|
25712
25596
|
readNamesOpt,
|
|
25713
25597
|
labelRows,
|
|
25598
|
+
validateWalkBag,
|
|
25714
25599
|
BaseResource,
|
|
25715
25600
|
BillingCodesResource,
|
|
25716
25601
|
BillingItemsResource,
|