node-autotask 0.37.1 → 0.38.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/MCP_TOOL_CATALOG.json +127 -44
  3. package/MCP_TOOL_MANIFEST.md +8 -8
  4. package/N8N_MCP_MANIFEST.json +4 -4
  5. package/N8N_MCP_MANIFEST.md +5 -5
  6. package/README.md +3 -0
  7. package/capabilities.json +2454 -1698
  8. package/dist/capabilities.cjs +2 -2
  9. package/dist/capabilities.js +1 -1
  10. package/dist/{chunk-7IADHNBZ.js → chunk-4QHGCYEI.js} +2255 -1946
  11. package/dist/chunk-5VHSLJMR.js +71 -0
  12. package/dist/{chunk-HBYKTXN3.cjs → chunk-73JMTV4O.cjs} +2453 -1697
  13. package/dist/{chunk-PFFW4DIK.cjs → chunk-7FNKFNIJ.cjs} +285 -285
  14. package/dist/{chunk-25VGFT5M.cjs → chunk-DCTUBUIZ.cjs} +143 -143
  15. package/dist/{chunk-7WI7FBPB.js → chunk-IY3WCPCK.js} +2 -2
  16. package/dist/{chunk-TBFXRNNE.cjs → chunk-IY5QZZVF.cjs} +2389 -2080
  17. package/dist/{chunk-PFJAMFUL.js → chunk-KPYXO7F5.js} +2 -2
  18. package/dist/chunk-MJTKK3QG.cjs +71 -0
  19. package/dist/{chunk-KQ6HMBB4.js → chunk-U3K6IDKB.js} +2453 -1697
  20. package/dist/{index-CqepIQfp.d.cts → index-BFbUB5YD.d.cts} +28 -8
  21. package/dist/{index-m-7fOwFl.d.cts → index-BQVStxwi.d.cts} +15 -1
  22. package/dist/{index-m-7fOwFl.d.ts → index-BQVStxwi.d.ts} +15 -1
  23. package/dist/{index-BDqw377y.d.ts → index-Q-4HQEEb.d.ts} +28 -8
  24. package/dist/index.cjs +27 -22
  25. package/dist/index.d.cts +26 -5
  26. package/dist/index.d.ts +26 -5
  27. package/dist/index.js +9 -4
  28. package/dist/operations/index.cjs +247 -227
  29. package/dist/operations/index.d.cts +16 -4
  30. package/dist/operations/index.d.ts +16 -4
  31. package/dist/operations/index.js +58 -38
  32. package/dist/pool.cjs +10 -9
  33. package/dist/pool.d.cts +2 -2
  34. package/dist/pool.d.ts +2 -2
  35. package/dist/pool.js +5 -4
  36. package/dist/resources/index.cjs +5 -4
  37. package/dist/resources/index.d.cts +2 -2
  38. package/dist/resources/index.d.ts +2 -2
  39. package/dist/resources/index.js +4 -3
  40. package/dist/types/index.d.cts +1 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/untrusted.cjs +24 -70
  43. package/dist/untrusted.js +12 -58
  44. package/docs/API.md +101 -0
  45. package/examples/mcp/manifest.ts +5 -0
  46. package/examples/mcp/rank.ts +62 -0
  47. package/examples/mcp/tools-meta.ts +6 -1
  48. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  CAPABILITIES
3
- } from "./chunk-KQ6HMBB4.js";
3
+ } from "./chunk-U3K6IDKB.js";
4
+ import {
5
+ wrapUntrusted
6
+ } from "./chunk-5VHSLJMR.js";
4
7
 
5
8
  // src/auth.ts
6
9
  var AUTH_HEADERS = {
@@ -2067,7 +2070,7 @@ function validateCondition(cond, path) {
2067
2070
  }
2068
2071
  if (op === "in" || op === "notIn") {
2069
2072
  if (!Array.isArray(cond.value)) {
2070
- throw new AutotaskConfigError(`Operator "${op}" requires an array "value" at ${path}`);
2073
+ throw new AutotaskConfigError(`Operator "${op}" requires an array or comma-separated string "value" at ${path}`);
2071
2074
  }
2072
2075
  }
2073
2076
  if (cond.value !== void 0) {
@@ -2125,7 +2128,7 @@ function validateFilter(filter) {
2125
2128
  }
2126
2129
  }
2127
2130
  function buildFilter(conditions) {
2128
- const list = Array.isArray(conditions) ? conditions : [conditions];
2131
+ const list = normalizeInValues(Array.isArray(conditions) ? conditions : [conditions]);
2129
2132
  if (list.length === 0) {
2130
2133
  throw new AutotaskConfigError("buildFilter requires at least one condition");
2131
2134
  }
@@ -2136,12 +2139,32 @@ function buildFilter(conditions) {
2136
2139
  }
2137
2140
  function normalizedConditions(filter) {
2138
2141
  if (filter === void 0 || filter === null) return void 0;
2139
- if (Array.isArray(filter)) return filter;
2140
- if (isRecord2(filter) && Array.isArray(filter.filter)) return filter.filter;
2142
+ if (Array.isArray(filter)) return normalizeInValues(filter);
2143
+ if (isRecord2(filter) && Array.isArray(filter.filter)) return normalizeInValues(filter.filter);
2141
2144
  throw new AutotaskConfigError(
2142
2145
  `filter must be a conditions array or { filter: [...] } (e.g. { filter: [{ field: 'status', op: 'eq', value: 'Open' }] }) \u2014 got ${refusedFilterShape(filter)}`
2143
2146
  );
2144
2147
  }
2148
+ function normalizeInValues(conditions, path = "filter") {
2149
+ const out = conditions.map((cond, i) => normalizeInCondition(cond, `${path}[${i}]`));
2150
+ return out.every((cond, i) => cond === conditions[i]) ? conditions : out;
2151
+ }
2152
+ function normalizeInCondition(cond, path) {
2153
+ if (!isRecord2(cond)) return cond;
2154
+ if (Array.isArray(cond.items)) {
2155
+ const items = normalizeInValues(cond.items, `${path}.items`);
2156
+ return items === cond.items ? cond : { ...cond, items };
2157
+ }
2158
+ if (cond.op !== "in" && cond.op !== "notIn" || typeof cond.value !== "string") return cond;
2159
+ const tokens = cond.value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
2160
+ if (tokens.length === 0) {
2161
+ throw new AutotaskConfigError(
2162
+ `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")`
2163
+ );
2164
+ }
2165
+ const value = cond.udf === true ? tokens : tokens.map((t) => String(Number(t)) === t ? Number(t) : t);
2166
+ return { ...cond, value };
2167
+ }
2145
2168
  function refusedFilterShape(filter) {
2146
2169
  if (isRecord2(filter) && !("filter" in filter)) return "an object missing the filter field";
2147
2170
  if (isRecord2(filter)) return "an object whose filter is not a conditions array";
@@ -2149,6 +2172,19 @@ function refusedFilterShape(filter) {
2149
2172
  if (t === "object" || t === "function") return "an object";
2150
2173
  return t === "string" ? "a string" : `a ${t}`;
2151
2174
  }
2175
+ function assertNoRelationLeaf(conditions, operation) {
2176
+ if (!Array.isArray(conditions)) return;
2177
+ for (const c of conditions) {
2178
+ if (!isRecord2(c)) continue;
2179
+ if (Array.isArray(c.items)) assertNoRelationLeaf(c.items, operation);
2180
+ else if (c.udf !== true && typeof c.field === "string" && c.field.includes(".")) {
2181
+ throw new AutotaskConfigError(
2182
+ `${operation}: filter field "${c.field}" is a relation filter that was not resolved on this path \u2014 the request was not issued`,
2183
+ { operation, suggestedAction: "Use a query surface that resolves relation filters (query, queryAll, queryBounded, count, operations.aggregate), or filter by the <relation>ID field directly." }
2184
+ );
2185
+ }
2186
+ }
2187
+ }
2152
2188
 
2153
2189
  // src/http/request-builders.ts
2154
2190
  function assertSafePath(path) {
@@ -17452,6 +17488,7 @@ var HttpClient = class {
17452
17488
  const url = buildUrl(this.baseUrl, opts.path, opts.query);
17453
17489
  const headers = withAuthHeaders(opts.headers, this.auth);
17454
17490
  assertCredentialShape(headers, this.auth);
17491
+ if (opts.path.includes("/query")) assertNoQueryRelationLeaf(opts);
17455
17492
  if (opts.body !== void 0) headers["Content-Type"] = "application/json";
17456
17493
  const bodyText = opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
17457
17494
  const correlationId = newCorrelationId();
@@ -17539,6 +17576,20 @@ var HttpClient = class {
17539
17576
  }
17540
17577
  }
17541
17578
  };
17579
+ function assertNoQueryRelationLeaf(opts) {
17580
+ const operation = opts.operation ?? opts.path;
17581
+ const body = opts.body;
17582
+ if (body !== null && typeof body === "object") assertNoRelationLeaf(body.filter, operation);
17583
+ const search = opts.query?.["search"];
17584
+ if (typeof search !== "string") return;
17585
+ let parsed;
17586
+ try {
17587
+ parsed = JSON.parse(search);
17588
+ } catch {
17589
+ return;
17590
+ }
17591
+ if (parsed !== null && typeof parsed === "object") assertNoRelationLeaf(parsed.filter, operation);
17592
+ }
17542
17593
 
17543
17594
  // src/operations/requiredFields.ts
17544
17595
  function effectiveRequiredFields(record, zoneFacts) {
@@ -17663,395 +17714,1227 @@ function resolveFieldsPreset(name, resourceKey, explicitIncludeFields) {
17663
17714
  return [...preset.fields];
17664
17715
  }
17665
17716
 
17666
- // src/resources/base/types.ts
17667
- var BOUNDED_QUERY_DEFAULT_LIMIT = 500;
17668
- var BOUNDED_QUERY_MAX_LIMIT = 2500;
17669
- var LATEST_MAX_COUNT_PROBES = 64;
17670
- var MARKER_RECOVERY_LIMIT = 5;
17671
-
17672
- // src/resources/base/helpers.ts
17673
- function describeResponseShape(body) {
17674
- if (body === null) return "null";
17675
- if (Array.isArray(body)) return `array(len=${body.length})`;
17676
- if (typeof body !== "object") return typeof body;
17677
- const rec = body;
17678
- const keys = Object.keys(rec).sort();
17679
- const typeOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
17680
- const parts = keys.slice(0, 16).map((k) => `${k}:${typeOf(rec[k])}`);
17681
- const more = keys.length > 16 ? `, +${keys.length - 16} more` : "";
17682
- return `{ ${parts.join(", ")}${more} }`;
17683
- }
17684
- function defaultMarkerToken(data) {
17685
- for (const field of ["name", "title"]) {
17686
- const v = data[field];
17687
- if (typeof v === "string" && v.trim() !== "") return v.trim();
17717
+ // src/operations/expandResourceNames.ts
17718
+ var EXPAND_RESOURCE_NAMES_MAX_IDS = 500;
17719
+ function composeName(record) {
17720
+ const parts = [];
17721
+ for (const field of ["firstName", "middleName", "lastName"]) {
17722
+ const value = record[field];
17723
+ if (typeof value === "string" && value.trim() !== "") parts.push(value.trim());
17688
17724
  }
17689
- return null;
17690
- }
17691
- var CONTEXT_ABSENT_MARKER = "#context-absent";
17692
- function contextAbsentKey(path) {
17693
- return path + "\0context-absent";
17694
- }
17695
- function noteCharsTruncationMarker(charsRemoved) {
17696
- return `\u2026 [+${charsRemoved} chars truncated]`;
17697
- }
17698
- function truncateNoteChars(items, cap, fields) {
17699
- let truncated = 0;
17700
- const out = items.map((item) => {
17701
- if (!isRecord2(item)) return item;
17702
- let changed = false;
17703
- const copy = { ...item };
17704
- for (const field of fields) {
17705
- const v = copy[field];
17706
- if (typeof v === "string" && v.length > cap) {
17707
- const end = cap > 0 && (v.charCodeAt(cap - 1) & 64512) === 55296 ? cap - 1 : cap;
17708
- copy[field] = v.slice(0, end) + noteCharsTruncationMarker(v.length - end);
17709
- truncated += 1;
17710
- changed = true;
17711
- }
17712
- }
17713
- return changed ? copy : item;
17714
- });
17715
- return [out, truncated];
17716
- }
17717
- function blindEmptyReason(stamp) {
17718
- return {
17719
- surface: stamp.surface,
17720
- kind: "blind-surface",
17721
- note: stamp.note,
17722
- authority: stamp.authority,
17723
- remedy: "the note is the remedy \u2014 the authority op answers for a record"
17724
- };
17725
- }
17726
- function attachUpdateTargetMissReason(err, key) {
17727
- const pinned = EMPTY_REASON_STAMPS[key]?.update;
17728
- if (pinned === void 0) return;
17729
- err.details = { ...err.details ?? {}, emptyReason: pinned };
17725
+ return parts.length > 0 ? parts.join(" ") : null;
17730
17726
  }
17731
- function buildDryRunResult(spec, policy = { redactPersonalData: true }) {
17732
- if (spec.operation === "") {
17733
- throw new AutotaskConfigError("dry-run: the operation name is required");
17727
+ async function expandResourceNames(client, ids2, opts = {}) {
17728
+ if (opts.maxIds !== void 0 && (!Number.isInteger(opts.maxIds) || opts.maxIds < 1)) {
17729
+ throw new AutotaskConfigError(`expandResourceNames: maxIds must be an integer >= 1 (got ${String(opts.maxIds)})`);
17734
17730
  }
17735
- const wouldApply = spec.checks.every((check) => check.ok);
17736
- const resource = spec.operation.split(".")[0] ?? spec.operation;
17737
- const diffPart = spec.diff !== void 0 ? {
17738
- diff: spec.diff.map((entry) => {
17739
- if (entry.to === void 0) return entry;
17740
- const to = isRedactedFieldName(entry.field, policy) ? REDACTED : redactResponseBody(entry.to, policy);
17741
- return { ...entry, to };
17742
- })
17743
- } : {};
17744
- const dataPart = spec.data !== void 0 ? { data: redactResponseBody(spec.data, policy) } : {};
17745
- return {
17746
- operation: spec.operation,
17747
- wouldApply,
17748
- target: { resource, ids: spec.ids },
17749
- request: { method: spec.method, path: spec.path },
17750
- ...diffPart,
17751
- checks: spec.checks,
17752
- impact: spec.impact,
17753
- simulated: true,
17754
- warnings: spec.warnings ?? [],
17755
- ...dataPart,
17756
- ...spec.emptyReason !== void 0 ? { emptyReason: spec.emptyReason } : {}
17757
- };
17758
- }
17759
- function computeFieldDiff(current, data) {
17760
- const out = [];
17761
- for (const [field, to] of Object.entries(data)) {
17762
- if (field === "id" || to === void 0) continue;
17763
- const from = current[field];
17764
- if (from !== void 0 && JSON.stringify(from) === JSON.stringify(to)) continue;
17765
- out.push({ field, from: from === void 0 ? null : from, to });
17731
+ const cap = Math.min(EXPAND_RESOURCE_NAMES_MAX_IDS, opts.maxIds ?? EXPAND_RESOURCE_NAMES_MAX_IDS);
17732
+ if (!Array.isArray(ids2) || ids2.length === 0) {
17733
+ throw new AutotaskConfigError(
17734
+ "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)",
17735
+ { suggestedAction: "Pass at least one vendor numeric resource id \u2014 safe integer, or the canonical integer string for ids above 2^53." }
17736
+ );
17766
17737
  }
17767
- return out;
17768
- }
17769
- function mergePartialBody(current, data) {
17770
- const merged = { ...current };
17771
- for (const [key, value] of Object.entries(data)) {
17772
- if (value === void 0) continue;
17773
- merged[key] = value;
17738
+ if (ids2.length > cap) {
17739
+ throw new AutotaskConfigError(
17740
+ `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)`,
17741
+ { suggestedAction: `Split the call into groups of <= ${cap} ids.` }
17742
+ );
17774
17743
  }
17775
- return merged;
17776
- }
17777
- function normalizeUdfInput(value) {
17778
- if (Array.isArray(value)) {
17779
- return value.map((entry) => {
17780
- if (!isRecord2(entry) || typeof entry.name !== "string" || entry.name === "") {
17781
- throw new AutotaskConfigError("userDefinedFields array entries must be { name, value } objects");
17744
+ const problems = [];
17745
+ const canonical = [];
17746
+ for (let i = 0; i < ids2.length; i += 1) {
17747
+ const at = `ids[${i}]`;
17748
+ const raw = ids2[i];
17749
+ try {
17750
+ canonical[i] = parseIdentifier(raw, at);
17751
+ } catch {
17752
+ if (typeof raw === "string") {
17753
+ 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)`);
17754
+ } else if (typeof raw === "number") {
17755
+ 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)`);
17756
+ } else {
17757
+ problems.push(`${at}: required \u2014 the vendor numeric resource id (safe integer, or the canonical integer string for ids above 2^53)`);
17782
17758
  }
17783
- return { name: entry.name, value: entry.value };
17784
- });
17785
- }
17786
- if (isRecord2(value)) {
17787
- return Object.entries(value).map(([name, entryValue]) => ({ name, value: entryValue }));
17788
- }
17789
- throw new AutotaskConfigError("userDefinedFields must be an array of { name, value } entries or a name-to-value map");
17790
- }
17791
- function blindStampFor(stamp, filter) {
17792
- if (stamp === void 0) return void 0;
17793
- if (stamp.kind !== "unfiltered-blind") return stamp;
17794
- const conditions = normalizedConditions(filter);
17795
- return conditions === void 0 || conditions.length === 0 ? stamp : void 0;
17796
- }
17797
- function decodeFileData(body) {
17798
- if (isRecord2(body)) {
17799
- const data = body.data;
17800
- if (typeof data === "string") return Buffer.from(data, "base64");
17801
- if (Array.isArray(data) && data.every((b) => typeof b === "number")) {
17802
- return Buffer.from(data);
17803
17759
  }
17804
17760
  }
17805
- if (typeof body === "string") return Buffer.from(body, "base64");
17806
- throw new AutotaskError(
17807
- "Expected the file envelope FileQueryResultModel { data } (or a base64 string); got a different shape",
17808
- { code: "INVALID_RESPONSE", category: "server" }
17809
- );
17810
- }
17811
- var DEPLURAL_EXCEPTIONS = {
17812
- tag_aliase: "tag_alias"
17813
- };
17814
- function entityNameFromPath(resourcePath) {
17815
- const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
17816
- const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
17817
- if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
17818
- if (snake.endsWith("s")) {
17819
- const stripped = snake.slice(0, -1);
17820
- return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
17821
- }
17822
- return snake;
17823
- }
17824
-
17825
- // src/resources/base/query.ts
17826
- var QueryBase = class {
17827
- http;
17828
- config;
17829
- /** Default page size for query walks (the client's resolved `pageSize`). */
17830
- defaultPageSize;
17831
- /** Versioned global URL segment (from the config), e.g. '/V1.0/Companies'. */
17832
- get resourcePath() {
17833
- return this.config.resourcePath;
17834
- }
17835
- /** The vendor entity this resource's ids identify (issue #116 — the `details.idRefs` label, e.g. `company`). */
17836
- get entityName() {
17837
- return entityNameFromPath(this.resourcePath);
17838
- }
17839
- /** Parent-scoped template (from the config); undefined for top-level resources. */
17840
- get parentScopedPath() {
17841
- return this.config.parentScopedPath;
17842
- }
17843
- /** Parent field used to narrow the global query (from the config). */
17844
- get parentField() {
17845
- return this.config.parentField;
17846
- }
17847
- /** The bounded client-scan caps (section 19.2; resolved per client). */
17848
- get resolutionCaps() {
17849
- return this.http.resolution;
17761
+ if (problems.length > 0) {
17762
+ throw new AutotaskConfigError(`expandResourceNames: ${problems.length} problem(s) found pre-request (zero wire was issued)
17763
+ - ` + problems.join("\n- "), {
17764
+ 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)."
17765
+ });
17850
17766
  }
17851
- constructor(http, config, pageSize) {
17852
- if (config.key === "" || config.resourcePath === "") {
17853
- throw new AutotaskConfigError("BaseResource requires a non-empty key and resourcePath");
17767
+ const unique = [];
17768
+ const seen = /* @__PURE__ */ new Set();
17769
+ for (const id of canonical) {
17770
+ const k = String(id);
17771
+ if (!seen.has(k)) {
17772
+ seen.add(k);
17773
+ unique.push(id);
17854
17774
  }
17855
- this.http = http;
17856
- this.config = config;
17857
- this.defaultPageSize = clampPageSize(pageSize ?? DEFAULT_PAGE_SIZE);
17858
17775
  }
17859
- // ------------------------------------------------------------------
17860
- // Query walk (wired to the G3 walker; cursor-follow per the pinned wire)
17861
- // ------------------------------------------------------------------
17862
- /** One page fetch: page 1 POSTs the QueryModel body; later pages POST the same body (`buildQueryBody`) to the absolute cursor (issue #118). */
17863
- async fetchPage(params, page, pageSize, state) {
17864
- const operation = `${this.config.key}.query`;
17865
- if (page === 1) {
17866
- const req = buildQueryRequest(this.http.baseUrl, this.resourcePath, { ...params, maxRecords: pageSize });
17867
- const body2 = await this.http.request({ ...req, operation });
17868
- return this.readPage(body2, page, pageSize, state);
17869
- }
17870
- const cursor = state.cursor;
17871
- if (cursor === null) {
17872
- throw new AutotaskError(`No pagination cursor available for page ${page} of ${operation}`, {
17873
- code: "INVALID_RESPONSE",
17874
- category: "server",
17875
- operation
17876
- });
17776
+ const holder = client.resources;
17777
+ const cache = client.http.resourceNameCache;
17778
+ const fresh = opts.fresh === true;
17779
+ const byId = /* @__PURE__ */ new Map();
17780
+ for (const id of unique) {
17781
+ const k = String(id);
17782
+ if (cache !== void 0 && !fresh) {
17783
+ const shared = cache.inflight(id);
17784
+ if (shared !== void 0) {
17785
+ let joined;
17786
+ try {
17787
+ joined = await shared;
17788
+ } catch (err) {
17789
+ if (!isAutotaskError(err) || err.code !== "ABORTED") throw err;
17790
+ }
17791
+ if (joined !== void 0) {
17792
+ byId.set(k, joined);
17793
+ continue;
17794
+ }
17795
+ }
17796
+ const hit = cache.get(id);
17797
+ if (hit !== void 0) {
17798
+ byId.set(k, { name: hit.name, blind: hit.blind });
17799
+ continue;
17800
+ }
17877
17801
  }
17878
- const body = buildQueryBody({ ...params, maxRecords: pageSize });
17879
- const res = await this.http.request({
17880
- method: "POST",
17881
- path: cursorToPath(this.http.baseUrl, cursor),
17882
- body,
17883
- operation,
17884
- idempotent: true
17885
- });
17886
- return this.readPage(res, page, pageSize, state);
17887
- }
17888
- /** @internal */
17889
- readPage(body, page, pageSize, state) {
17890
- const { items, pageDetails } = unwrapItems(body, `${this.config.key}.query`);
17891
- state.cursor = pageDetails.nextPageUrl ?? null;
17892
- const hasMore = state.cursor !== null;
17893
- return {
17894
- items,
17895
- page,
17896
- pageSize,
17897
- // Verbatim vendor echo of MaxRecords — NOT a match count (live-verified).
17898
- requestCount: pageDetails.requestCount,
17899
- // No vendor total exists: the pages known so far (exact on the last page).
17900
- totalPages: hasMore ? page + 1 : items.length === 0 ? page - 1 : page,
17901
- hasMore
17902
- };
17802
+ const epoch = cache?.epoch ?? 0;
17803
+ const work = (async () => {
17804
+ try {
17805
+ const record = await holder.get(id, { fresh });
17806
+ const name = composeName(record);
17807
+ if (cache !== void 0 && name !== null) cache.set(id, { name, blind: false }, epoch);
17808
+ return { name, blind: false };
17809
+ } catch (err) {
17810
+ if (err instanceof NotFoundError) {
17811
+ const entry = { name: null, blind: true };
17812
+ if (cache !== void 0) cache.set(id, entry, epoch);
17813
+ return { name: null, blind: true };
17814
+ }
17815
+ if (isAutotaskError(err) && err.code !== "ABORTED") {
17816
+ const e = { code: err.code, message: err.message };
17817
+ if (err.httpStatus !== void 0) e.httpStatus = err.httpStatus;
17818
+ return { name: null, blind: false, error: e };
17819
+ }
17820
+ throw err;
17821
+ }
17822
+ })();
17823
+ const settled = cache !== void 0 ? await cache.track(id, work) : await work;
17824
+ byId.set(k, settled);
17903
17825
  }
17904
- /** Build the per-walk fetcher (closure keeps cursor state private to this walk). */
17905
- pageFetcher(params) {
17906
- const state = { cursor: null };
17907
- const p = params ?? {};
17908
- return (page, pageSize) => this.fetchPage(p, page, pageSize, state);
17826
+ const results = [];
17827
+ for (const id of canonical) {
17828
+ const out = byId.get(String(id));
17829
+ results.push({ id, name: out.name, blind: out.blind, ...out.error !== void 0 ? { error: out.error } : {} });
17909
17830
  }
17910
- /** @internal */
17911
- walkPageSize(params) {
17912
- return clampPageSize(params?.maxRecords ?? this.defaultPageSize);
17831
+ const resolved = results.filter((r) => r.name !== null).length;
17832
+ const blind = results.filter((r) => r.blind === true).length;
17833
+ const failed = results.filter((r) => r.error !== void 0).length;
17834
+ return { results, resolved, blind, failed };
17835
+ }
17836
+
17837
+ // src/operations/entityType.ts
17838
+ function buildEntityTypeMap() {
17839
+ const exact = {};
17840
+ for (const [resource, entry] of Object.entries(RELATIONSHIP_MAP)) {
17841
+ const apiEntity = entry.apiEntity;
17842
+ if (typeof apiEntity !== "string" || apiEntity === "") continue;
17843
+ if (exact[apiEntity] === void 0) exact[apiEntity] = resource;
17913
17844
  }
17914
- /** Stream items across pages (the resource `query` surface). */
17915
- queryItems(params) {
17916
- return paginateItems(this.pageFetcher(params), { pageSize: this.walkPageSize(params) });
17845
+ const aliases = {};
17846
+ for (const [apiEntity, resource] of Object.entries(exact)) {
17847
+ if (!apiEntity.endsWith("s") || apiEntity.endsWith("ss")) continue;
17848
+ const singular = apiEntity.slice(0, -1);
17849
+ if (exact[singular] !== void 0 || aliases[singular] !== void 0) continue;
17850
+ aliases[singular] = resource;
17917
17851
  }
17918
- /** Stream page objects (the resource `queryPages` surface). */
17919
- queryPageIter(params) {
17920
- return paginate(this.pageFetcher(params), { pageSize: this.walkPageSize(params) });
17852
+ return { ...exact, ...aliases };
17853
+ }
17854
+ var ENTITY_TYPE_TO_RESOURCE = buildEntityTypeMap();
17855
+ var ENUMERABLE_RESOURCES = new Set(
17856
+ CAPABILITIES.filter((rec) => rec.resource !== "operations" && (rec.operation === "query" || rec.operation === "get")).map(
17857
+ (rec) => rec.resource
17858
+ )
17859
+ );
17860
+ function resourceForEntityType(entityType) {
17861
+ if (typeof entityType !== "string" || entityType === "") return null;
17862
+ return ENTITY_TYPE_TO_RESOURCE[entityType] ?? null;
17863
+ }
17864
+ function isEnumerableResource(resource) {
17865
+ if (typeof resource !== "string" || resource === "") return false;
17866
+ return ENUMERABLE_RESOURCES.has(resource);
17867
+ }
17868
+
17869
+ // src/operations/types.ts
17870
+ var WORKFLOW_RESOURCES = [
17871
+ "companies",
17872
+ "tickets",
17873
+ "contacts",
17874
+ "configuration_items",
17875
+ "projects"
17876
+ ];
17877
+
17878
+ // src/operations/fieldCatalog.ts
17879
+ var MAX_FIELD_CATALOG_WIRE = 60;
17880
+ 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.";
17881
+ var FIELD_CATALOG_OPT_KEYS = ["resources", "include", "fresh", "fields", "labels"];
17882
+ function projectFieldRow(raw, labels = false, udf = false) {
17883
+ const r = typeof raw === "object" && raw !== null ? raw : {};
17884
+ const name = typeof r.name === "string" ? r.name : "";
17885
+ const type = typeof r.dataType === "string" ? r.dataType : typeof r.type === "string" ? r.type : "";
17886
+ const values = labels && r.isPickList === true && Array.isArray(r.picklistValues) && r.picklistValues.length > 0 ? r.picklistValues : void 0;
17887
+ const referenceEntityType = typeof r.referenceEntityType === "string" ? r.referenceEntityType : "";
17888
+ const targetResource = resourceForEntityType(referenceEntityType);
17889
+ return {
17890
+ name,
17891
+ ...typeof r.label === "string" && r.label !== "" ? { label: r.label } : {},
17892
+ type,
17893
+ isRequired: r.isRequired === true,
17894
+ isReadOnly: r.isReadOnly === true,
17895
+ // Issue #219 N7: a UDF row never serves the vendor's isQueryable (it is
17896
+ // true for UDFs while a plain filter on a UDF name is refused); it serves
17897
+ // the marker the filter actually needs.
17898
+ ...udf ? { filterable: "udf-marker" } : { isQueryable: r.isQueryable === true },
17899
+ isPickList: r.isPickList === true,
17900
+ ...isObjectOrArrayType(type) ? { writeShape: "replace-all" } : {},
17901
+ maxLength: typeof r.length === "number" ? r.length : null,
17902
+ referenceEntityType,
17903
+ targetResource,
17904
+ targetEnumerable: isEnumerableResource(targetResource),
17905
+ ...values !== void 0 ? { picklistValues: values } : {}
17906
+ };
17907
+ }
17908
+ function isObjectOrArrayType(type) {
17909
+ const t = type.trim().toLowerCase();
17910
+ return t === "object" || t === "array" || t === "collection" || t.endsWith("[]");
17911
+ }
17912
+ function exposesPicklistValues(raw) {
17913
+ const r = typeof raw === "object" && raw !== null ? raw : {};
17914
+ return Array.isArray(r.picklistValues) && r.picklistValues.length > 0;
17915
+ }
17916
+ function applyFieldsFilter(projected, filter, row) {
17917
+ const kept = filter === void 0 ? projected : projected.filter((f) => filter.includes(f.name));
17918
+ if (filter !== void 0) {
17919
+ const present = new Set(projected.map((f) => f.name));
17920
+ row.unmatchedFields = filter.filter((n) => !present.has(n));
17921
+ row.projected = true;
17921
17922
  }
17922
- /** Collect every item across all pages (the resource `queryAll` surface; 1000-page guarded). */
17923
- async queryAllItems(params) {
17924
- return collectAll(this.pageFetcher(params), { pageSize: this.walkPageSize(params), maxRecords: params?.maxTotalRecords });
17923
+ row.fields = kept;
17924
+ row.required = kept.filter((f) => f.isRequired).map((f) => f.name);
17925
+ row.readOnly = kept.filter((f) => f.isReadOnly).map((f) => f.name);
17926
+ row.queryable = kept.filter((f) => f.isQueryable).map((f) => f.name);
17927
+ row.picklistFields = kept.filter((f) => f.isPickList).map((f) => f.name);
17928
+ }
17929
+ function capabilitySets() {
17930
+ const fieldOps = /* @__PURE__ */ new Map();
17931
+ const udfCapable = /* @__PURE__ */ new Set();
17932
+ const resources = /* @__PURE__ */ new Set();
17933
+ for (const rec of CAPABILITIES) {
17934
+ if (rec.resource === "operations") continue;
17935
+ resources.add(rec.resource);
17936
+ if (rec.operation === "getFieldInfo") fieldOps.set(rec.resource, rec);
17937
+ if (rec.udfCapable) udfCapable.add(rec.resource);
17925
17938
  }
17926
- /**
17927
- * `queryBounded` — server-side pagination (the MCP query surface): ONE call
17928
- * walks the vendor cursor internally until `limit` TOTAL records are
17929
- * collected or the cursor is exhausted, and returns a bounded,
17930
- * complete-enough result. The client never owns the walk:
17931
- * - `limit` = total records (default 500, hard max 2500 — above it throws
17932
- * AutotaskConfigError; it is NOT a page size). The page-1 request uses
17933
- * the internal page size min(500, limit) (the `latest` mechanism sizes
17934
- * its walk from the matching count); every follow-up page POSTs the
17935
- * SAME QueryModel body (`buildQueryBody`: MaxRecords, IncludeFields and
17936
- * the preserved filter) to the absolute cursor (issue #118 — the vendor
17937
- * honours IncludeFields there and ignores MaxRecords, serving its own
17938
- * page size), so the walk reuses `fetchPage` (the shared cursor machinery)
17939
- * with a per-walk closure state (walks never share state).
17940
- * - The 1000-page guard still applies as a SOFT stop: a walk that would
17941
- * fetch a 1001st page stops with `hasMore: true` (never a throw — this
17942
- * is a bounded read, not the `paginate` generator's error path).
17943
- * - `hasMore` = (truncated by limit OR guard) AND (a non-null next cursor
17944
- * was seen at the stop point OR the final page was sliced to meet the
17945
- * limit). Cursor exhausted, or a short/empty page with no truncation →
17946
- * false (never claims more than the cursor + the slice say exist).
17947
- * - `count` is always omitted: `pageDetails.count` is the current page's
17948
- * record count (vendor-documented), never a total — `<resource>.count`
17949
- * is the total.
17950
- * - Issue #59: `latest: N` (validated pre-request, zero wire) delegates to
17951
- * `latestWalk` — the count-anchored N-highest-id mechanism; ABSENT, the
17952
- * walk below is bit-for-bit the existing path.
17953
- * Pure read; no mutation paths involved.
17954
- */
17955
- async queryBounded(params) {
17956
- if (Array.isArray(params)) {
17957
- throw new AutotaskConfigError(
17958
- "queryBounded: params must be an object { filter?, limit?, latest?, includeFields? } \u2014 a bare conditions array is not accepted (wrap it: { filter: [...] })"
17959
- );
17939
+ return { fieldOps, udfCapable, resources };
17940
+ }
17941
+ var derivedIndex = null;
17942
+ function derivedFieldsFor(resource) {
17943
+ if (derivedIndex === null) {
17944
+ derivedIndex = /* @__PURE__ */ new Map();
17945
+ for (const rec of CAPABILITIES) {
17946
+ const declared = rec.metadata?.derivedFields;
17947
+ if (!Array.isArray(declared) || derivedIndex.has(rec.resource)) continue;
17948
+ const rows = [];
17949
+ for (const row of declared) {
17950
+ if (typeof row?.field !== "string" || typeof row.rule !== "string") continue;
17951
+ rows.push({
17952
+ field: row.field,
17953
+ rule: row.rule,
17954
+ ...typeof row.writableRoute === "string" ? { writableRoute: row.writableRoute } : {}
17955
+ });
17956
+ }
17957
+ derivedIndex.set(rec.resource, rows);
17960
17958
  }
17961
- const limit = params?.limit ?? BOUNDED_QUERY_DEFAULT_LIMIT;
17962
- if (!Number.isInteger(limit) || limit < 1 || limit > BOUNDED_QUERY_MAX_LIMIT) {
17963
- throw new AutotaskConfigError(
17964
- `queryBounded: limit must be an integer 1..${BOUNDED_QUERY_MAX_LIMIT} (got ${String(limit)}) \u2014 it is the TOTAL record cap, not a page size`
17959
+ }
17960
+ return derivedIndex.get(resource) ?? [];
17961
+ }
17962
+ function apiEntityFor(rec) {
17963
+ const m = /^GET \/V1\.0\/([^/]+)\/entityInformation\/fields$/.exec(rec.endpoint ?? "");
17964
+ if (m === null) {
17965
+ throw new AutotaskConfigError(
17966
+ `fieldCatalog: the plan's getFieldInfo endpoint for "${rec.resource}" has no /V1.0/<Entity>/entityInformation/fields shape (${rec.endpoint}) \u2014 plan defect`,
17967
+ { operation: "operations.fieldCatalog" }
17968
+ );
17969
+ }
17970
+ return m[1];
17971
+ }
17972
+ function validateCatalogOptsShape(opts) {
17973
+ if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
17974
+ throw new AutotaskConfigError(
17975
+ "fieldCatalog: opts must be an object { resources?: string[], include?: (info|fields|udf)[], fresh?: boolean, fields?: string[], labels?: boolean }"
17976
+ );
17977
+ }
17978
+ for (const key of Object.keys(opts ?? {})) {
17979
+ if (!FIELD_CATALOG_OPT_KEYS.includes(key)) {
17980
+ throw new AutotaskConfigError(
17981
+ `fieldCatalog: unknown opts key "${key}" \u2014 the bag is closed: ${FIELD_CATALOG_OPT_KEYS.join(", ")}`,
17982
+ { suggestedAction: "Drop the unknown key (autotask_describe(operations.fieldCatalog) shows the exact bag)." }
17965
17983
  );
17966
17984
  }
17967
- const noteChars = params?.noteChars;
17968
- if (noteChars !== void 0 && (!Number.isInteger(noteChars) || noteChars < 1)) {
17985
+ }
17986
+ }
17987
+ function resolveInclude(opts) {
17988
+ const includeRaw = opts?.include ?? ["fields"];
17989
+ if (!Array.isArray(includeRaw) || includeRaw.length === 0) {
17990
+ throw new AutotaskConfigError("fieldCatalog: opts.include must be a non-empty array of (info|fields|udf)");
17991
+ }
17992
+ const include = [];
17993
+ for (const kind of includeRaw) {
17994
+ if (typeof kind !== "string" || !FIELD_CACHE_KINDS.includes(kind)) {
17969
17995
  throw new AutotaskConfigError(
17970
- `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)`
17996
+ `fieldCatalog: unknown include kind "${String(kind)}" \u2014 the valid kinds are: ${FIELD_CACHE_KINDS.join(", ")}`,
17997
+ { suggestedAction: `Filter with one or more of: ${FIELD_CACHE_KINDS.join(", ")} (default ['fields']).` }
17971
17998
  );
17972
17999
  }
17973
- const walkParams = params?.fieldsPreset !== void 0 ? { ...params, includeFields: resolveFieldsPreset(params.fieldsPreset, this.config.key, params.includeFields) } : params ?? {};
17974
- const latest = walkParams.latest;
17975
- if (latest !== void 0) {
17976
- if (!Number.isInteger(latest) || latest < 1 || latest > BOUNDED_QUERY_MAX_LIMIT) {
17977
- throw new AutotaskConfigError(
17978
- `queryBounded: latest must be an integer 1..${BOUNDED_QUERY_MAX_LIMIT} (got ${String(latest)}) \u2014 it is the count of the highest-id rows to return`
17979
- );
17980
- }
17981
- if (params?.limit !== void 0 && limit < latest) {
18000
+ if (!include.includes(kind)) include.push(kind);
18001
+ }
18002
+ return include;
18003
+ }
18004
+ function resolveTargets(opts, fieldOps, allResources) {
18005
+ const resourcesRaw = opts?.resources ?? WORKFLOW_RESOURCES;
18006
+ if (!Array.isArray(resourcesRaw) || resourcesRaw.length === 0) {
18007
+ throw new AutotaskConfigError("fieldCatalog: opts.resources must be a non-empty array of plan resource keys");
18008
+ }
18009
+ const targets = [];
18010
+ for (const r of resourcesRaw) {
18011
+ if (typeof r !== "string" || r.trim() === "") {
18012
+ throw new AutotaskConfigError(
18013
+ `fieldCatalog: opts.resources must contain non-empty plan resource key strings (got ${typeof r === "string" && r.trim() === "" ? "a blank string" : typeof r})`
18014
+ );
18015
+ }
18016
+ const key = r.trim();
18017
+ const fieldOp = fieldOps.get(key);
18018
+ if (fieldOp === void 0) {
18019
+ if (allResources.has(key)) {
17982
18020
  throw new AutotaskConfigError(
17983
- `queryBounded: limit (${String(limit)}) must be >= latest (${String(latest)}) \u2014 latest governs the returned count and limit is the walk window`
18021
+ `fieldCatalog: resource "${key}" has no field catalog \u2014 the vendor defines the entityInformation trio for the ${fieldOps.size} plan entity resources only`,
18022
+ { suggestedAction: `Pass one of the ${fieldOps.size} field-capable plan keys (list them with autotask_catalog).` }
17984
18023
  );
17985
18024
  }
17986
- return this.applyNoteChars(await this.latestWalk(walkParams, Math.max(limit, latest), latest), noteChars);
18025
+ throw new AutotaskError(
18026
+ `fieldCatalog: unknown resource "${key}" \u2014 the plan has ${fieldOps.size} field-capable resources`,
18027
+ {
18028
+ code: "RESOURCE_UNKNOWN",
18029
+ category: "validation",
18030
+ suggestedAction: "Pass the exact plan resource key (snake_case) from autotask_catalog (e.g. tickets, companies, configuration_items)."
18031
+ }
18032
+ );
17987
18033
  }
17988
- const { items, pages, guard, sliced, cursor } = await this.walkBounded(walkParams, limit);
17989
- const hasMore = (items.length >= limit || guard) && (sliced > 0 || cursor !== null);
17990
- const result = { items, fetched: items.length, hasMore, pagesWalked: pages };
17991
- this.stampEmptyQueryResult(result, params);
17992
- return this.applyNoteChars(result, noteChars);
18034
+ if (!targets.includes(key)) targets.push(key);
17993
18035
  }
17994
- /** Issue #61 (slice A): the noteChars truncation at the SINGLE chokepoint
17995
- * of `queryBounded` — the normal walk and the `latest` paths both
17996
- * produced their `BoundedQueryResult` here (the single chokepoint the
17997
- * plan pins: R5). Copy-on-truncate (the input records are never
17998
- * mutated); the additive `noteCharsTruncated` stamp = the truncated-
17999
- * field count, ABSENT when 0. The resource's untrusted-class field set
18000
- * is the generated stamp (the SAME plan column the registry's
18001
- * `metadata.untrustedFields` channel stamps — zero second source of
18002
- * truth); an absent set = the cap is a no-op for the resource (bit-for-
18003
- * bit the pre-slice result).
18004
- * @internal */
18005
- applyNoteChars(result, noteChars) {
18006
- if (noteChars === void 0) return result;
18007
- const fields = UNTRUSTED_FIELDS_BY_RESOURCE[this.config.key];
18008
- if (fields === void 0 || fields.length === 0) return result;
18009
- const [items, truncated] = truncateNoteChars(result.items, noteChars, fields);
18010
- if (truncated > 0) {
18011
- return { ...result, items, noteCharsTruncated: truncated };
18036
+ return targets;
18037
+ }
18038
+ function resolveFieldsFilter(opts) {
18039
+ const fieldsRaw = opts?.fields;
18040
+ if (fieldsRaw === void 0) return void 0;
18041
+ if (!Array.isArray(fieldsRaw) || fieldsRaw.length === 0) {
18042
+ throw new AutotaskConfigError("fieldCatalog: opts.fields must be a non-empty array of vendor field name strings");
18043
+ }
18044
+ const names = [];
18045
+ for (const f of fieldsRaw) {
18046
+ if (typeof f !== "string" || f.trim() === "") {
18047
+ throw new AutotaskConfigError(
18048
+ `fieldCatalog: opts.fields must contain non-empty vendor field name strings (got ${typeof f === "string" && f.trim() === "" ? "a blank string" : typeof f})`
18049
+ );
18012
18050
  }
18013
- return result;
18051
+ const name = f.trim();
18052
+ if (!names.includes(name)) names.push(name);
18014
18053
  }
18015
- /** The bounded walk loop (shared by the normal `queryBounded` path and the
18016
- * `latest` modes): walks the vendor cursor until `limit` TOTAL records
18017
- * are collected or the cursor is exhausted; the 1000-page guard is a
18018
- * SOFT stop (never a throw). ONE per-walk closure (walks never share
18019
- * state).
18020
- * @internal */
18021
- async walkBounded(p, limit) {
18022
- const state = { cursor: null };
18023
- const items = [];
18024
- let pages = 0;
18025
- let sliced = 0;
18026
- let guard = false;
18027
- const pageSize = Math.min(MAX_PAGE_SIZE, limit);
18028
- for (; ; ) {
18029
- if (pages >= MAX_PAGES) {
18030
- guard = true;
18031
- break;
18054
+ return names;
18055
+ }
18056
+ function resolveLabels(opts) {
18057
+ if (opts !== void 0 && opts.labels !== void 0 && typeof opts.labels !== "boolean") {
18058
+ throw new AutotaskConfigError(
18059
+ `fieldCatalog: opts.labels must be a boolean (got ${typeof opts.labels})`
18060
+ );
18061
+ }
18062
+ return opts?.labels === true;
18063
+ }
18064
+ function assertWithinWireCap(targetCount, kindCount) {
18065
+ const cost = targetCount * kindCount;
18066
+ if (cost > MAX_FIELD_CATALOG_WIRE) {
18067
+ throw new AutotaskConfigError(
18068
+ `fieldCatalog: ${targetCount} resource(s) \xD7 ${kindCount} kind(s) = ${cost} metadata reads exceed the wire cap of ${MAX_FIELD_CATALOG_WIRE} \u2014 the call is refused above its cap (never truncated)`,
18069
+ { suggestedAction: `Pass a smaller resources list or a narrower include set so the product stays \u2264 ${MAX_FIELD_CATALOG_WIRE}.` }
18070
+ );
18071
+ }
18072
+ }
18073
+ function kindFetches(holder, resource) {
18074
+ return {
18075
+ info: () => holder?.getEntityInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getEntityInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" })),
18076
+ fields: () => holder?.getFieldInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getFieldInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" })),
18077
+ udf: () => holder?.getUdfInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getUdfInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" }))
18078
+ };
18079
+ }
18080
+ async function sweepResource(ctx, resource, tally) {
18081
+ const { client, include, fieldsFilter, labels, fresh } = ctx;
18082
+ const cache = client.http.fieldCache;
18083
+ const fieldOp = ctx.fieldOps.get(resource);
18084
+ if (fieldOp === void 0) return void 0;
18085
+ const apiEntity = apiEntityFor(fieldOp);
18086
+ const holder = client[camelKey(resource)];
18087
+ const row = {
18088
+ resource,
18089
+ apiEntity,
18090
+ udfCapable: ctx.udfCapable.has(resource),
18091
+ // Issue #219 N7: the UDF surface is declared with the mechanism, not a boolean.
18092
+ ...ctx.udfCapable.has(resource) ? { udfFilter: "udf-marker", udfWriteShape: "merge" } : {},
18093
+ unmatchedFields: [],
18094
+ required: [],
18095
+ readOnly: [],
18096
+ queryable: [],
18097
+ picklistFields: [],
18098
+ cache: { hit: false, age: null },
18099
+ wire: 0,
18100
+ error: null
18101
+ };
18102
+ const fetches = kindFetches(holder, resource);
18103
+ if (fresh) cache?.invalidate(resource);
18104
+ const projectList = (kind, list) => {
18105
+ const projected = list.map((f) => projectFieldRow(f, labels, kind === "udf"));
18106
+ if (kind === "fields") applyFieldsFilter(projected, fieldsFilter, row);
18107
+ else row.udf = projected;
18108
+ for (const f of list) if (exposesPicklistValues(f)) tally.anyPicklistValues = true;
18109
+ };
18110
+ let allCached = true;
18111
+ const ages = [];
18112
+ for (const kind of include) {
18113
+ const peek = cache !== void 0 && !fresh ? cache.peek(resource, kind) : { hit: false, age: null };
18114
+ if (peek.hit !== true) allCached = false;
18115
+ if (peek.hit === true && cache !== void 0) {
18116
+ const value2 = cache.get(resource, kind);
18117
+ if (kind === "info") row.info = value2 ?? null;
18118
+ else projectList(kind, Array.isArray(value2) ? value2 : []);
18119
+ if (peek.age !== null) ages.push(peek.age);
18120
+ continue;
18121
+ }
18122
+ let value;
18123
+ try {
18124
+ value = await fetches[kind]();
18125
+ } catch (err) {
18126
+ if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
18127
+ tally.wireUsed += 1;
18128
+ row.wire += 1;
18129
+ if (row.error === null) row.error = { code: err.code, message: err.message };
18130
+ continue;
18131
+ }
18132
+ tally.wireUsed += 1;
18133
+ row.wire += 1;
18134
+ if (kind === "info") {
18135
+ row.info = value ?? null;
18136
+ } else {
18137
+ if (!Array.isArray(value)) {
18138
+ if (row.error === null) row.error = { code: "INVALID_RESPONSE", message: `fieldCatalog: the ${kind} response for ${resource} was not a list` };
18139
+ continue;
18032
18140
  }
18033
- const page = await this.fetchPage(p, pages + 1, pageSize, state);
18034
- pages += 1;
18035
- const remaining = limit - items.length;
18036
- const take = page.items.slice(0, remaining);
18037
- sliced += Math.max(0, page.items.length - remaining);
18038
- items.push(...take);
18039
- if (items.length >= limit) break;
18040
- if (!page.hasMore || page.items.length === 0) break;
18141
+ projectList(kind, value);
18041
18142
  }
18042
- return { items, pages, guard, sliced, cursor: state.cursor };
18043
18143
  }
18044
- /** A walk stop is TRUNCATED when the 1000-page guard fired, or the limit
18045
- * was met with a live next cursor (or a sliced final page): rows exist
18046
- * past the stop point, and a top-N-by-id over the collected set alone is
18047
- * unprovable (the `latest` mechanism refuses instead of guessing).
18048
- * @internal */
18049
- walkTruncated(limit, stop) {
18050
- return stop.guard || stop.itemsLength >= limit && (stop.sliced > 0 || stop.cursor !== null);
18144
+ row.cache = allCached ? { hit: true, age: ages.length > 0 ? Math.max(...ages) : 0 } : { hit: false, age: null };
18145
+ const declared = derivedFieldsFor(resource);
18146
+ if (declared.length > 0) {
18147
+ const byField = new Map(declared.map((d) => [d.field, d]));
18148
+ row.derivedFields = declared;
18149
+ for (const field of row.fields ?? []) {
18150
+ const derivation = byField.get(field.name);
18151
+ if (derivation === void 0) continue;
18152
+ field.derived = {
18153
+ rule: derivation.rule,
18154
+ ...derivation.writableRoute !== void 0 ? { writableRoute: derivation.writableRoute } : {}
18155
+ };
18156
+ }
18051
18157
  }
18052
- /** The 0-item result stamping — the ONE source of truth (the normal walk
18053
- * and the `latest` empty outcome both ride it): the typed blind-surface
18054
- * sentinel + the pinned empty-reason, through the filter-aware predicate
18158
+ return row;
18159
+ }
18160
+ async function fieldCatalog(client, opts) {
18161
+ validateCatalogOptsShape(opts);
18162
+ const { fieldOps, udfCapable, resources: allResources } = capabilitySets();
18163
+ const include = resolveInclude(opts);
18164
+ const targets = resolveTargets(opts, fieldOps, allResources);
18165
+ const fieldsFilter = resolveFieldsFilter(opts);
18166
+ const labels = resolveLabels(opts);
18167
+ assertWithinWireCap(targets.length, include.length);
18168
+ const cache = client.http.fieldCache;
18169
+ const zone = client.config.zone ?? "unknown";
18170
+ const rows = [];
18171
+ const tally = { wireUsed: 0, anyPicklistValues: false };
18172
+ const ctx = { client, include, fieldsFilter, labels, fresh: opts?.fresh === true, fieldOps, udfCapable };
18173
+ for (const resource of targets) {
18174
+ const row = await sweepResource(ctx, resource, tally);
18175
+ if (row !== void 0) rows.push(row);
18176
+ }
18177
+ return {
18178
+ zone,
18179
+ label: zone,
18180
+ cached: cache !== void 0,
18181
+ labels,
18182
+ ttlMs: cache?.ttlMs ?? null,
18183
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
18184
+ rows,
18185
+ wireUsed: tally.wireUsed,
18186
+ cap: MAX_FIELD_CATALOG_WIRE,
18187
+ truncated: false,
18188
+ counts: {
18189
+ resources: targets.length,
18190
+ fieldCapableResources: fieldOps.size,
18191
+ udfCapableResources: udfCapable.size,
18192
+ cached: rows.filter((r) => r.cache.hit === true).length
18193
+ },
18194
+ honesty: {
18195
+ picklistValues: tally.anyPicklistValues ? "partial" : "unexposed",
18196
+ note: FIELD_CATALOG_HONESTY_NOTE
18197
+ }
18198
+ };
18199
+ }
18200
+
18201
+ // src/field-sets.ts
18202
+ var MODEL_FIELD_SETS = {
18203
+ "appointments": ["id", "createDateTime", "creatorResourceID", "description", "endDateTime", "resourceID", "startDateTime", "title", "updateDateTime"],
18204
+ "billing_codes": ["id", "afterHoursWorkType", "billingCodeType", "department", "description", "externalNumber", "generalLedgerAccount", "isActive", "isExcludedFromNewContracts", "markupRate", "name", "taxCategoryID", "unitCost", "unitPrice", "useType"],
18205
+ "billing_item_approval_levels": ["id", "approvalDateTime", "approvalLevel", "approvalResourceID", "timeEntryID"],
18206
+ "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"],
18207
+ "change_request_links": ["id", "changeRequestTicketID", "problemOrIncidentTicketID"],
18208
+ "checklist_libraries": ["id", "description", "entityType", "isActive", "name"],
18209
+ "checklist_library_checklist_items": ["id", "checklistLibraryID", "isImportant", "itemName", "knowledgebaseArticleID", "position", "soapParentPropertyId"],
18210
+ "classification_icons": ["id", "description", "isActive", "isSystem", "name", "relativeUrl"],
18211
+ "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"],
18212
+ "company_alerts": ["id", "alertText", "alertTypeID", "companyID", "soapParentPropertyId"],
18213
+ "company_locations": ["id", "address1", "address2", "alternatePhone1", "alternatePhone2", "city", "companyID", "countryID", "description", "fax", "isActive", "isPrimary", "isTaxExempt", "overrideCompanyTaxSettings", "name", "phone", "postalCode", "roundtripDistance", "state", "taxRegionID", "soapParentPropertyId"],
18214
+ "company_notes": ["id", "actionType", "assignedResourceID", "companyID", "completedDateTime", "contactID", "createDateTime", "endDateTime", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastModifiedDate", "name", "note", "opportunityID", "startDateTime", "soapParentPropertyId"],
18215
+ "company_site_configurations": ["id", "companyID", "locationName", "soapParentPropertyId", "userDefinedFields"],
18216
+ "company_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
18217
+ "configuration_item_billing_product_associations": ["id", "billingProductID", "configurationItemID", "effectiveDate", "expirationDate", "soapParentPropertyId"],
18218
+ "configuration_item_categories": ["id", "displayColorRGB", "isActive", "isClientPortalDefault", "isGlobalDefault", "name", "nickname"],
18219
+ "configuration_item_category_udf_associations": ["id", "configurationItemCategoryID", "isRequired", "userDefinedFieldDefinitionID", "soapParentPropertyId"],
18220
+ "configuration_item_dns_records": ["id", "createDateTime", "data", "installedProductID", "timeToLiveSeconds", "dnsType", "soapParentPropertyId"],
18221
+ "configuration_item_notes": ["id", "configurationItemID", "createDateTime", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "noteType", "title", "soapParentPropertyId"],
18222
+ "configuration_item_related_items": ["id", "configurationItemID", "relatedConfigurationItemID", "relationship", "soapParentPropertyId"],
18223
+ "configuration_item_ssl_subject_alternative_names": ["id", "configurationItemID", "name", "soapParentPropertyId"],
18224
+ "configuration_item_types": ["id", "isActive", "name"],
18225
+ "configuration_item_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
18226
+ "configuration_items": ["id", "apiVendorID", "configurationItemCategoryID", "companyID", "companyLocationID", "configurationItemType", "contactID", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "createdByPersonID", "dailyCost", "dattoAvailableKilobytes", "dattoDeviceMemoryMegabytes", "dattoDrivesErrors", "dattoHostname", "dattoInternalIP", "dattoKernelVersionID", "dattoLastCheckInDateTime", "dattoNICSpeedKilobitsPerSecond", "dattoNumberOfAgents", "dattoNumberOfDrives", "dattoNumberOfVolumes", "dattoOffsiteUsedBytes", "dattoOSVersionID", "dattoPercentageUsed", "dattoProtectedKilobytes", "dattoRemoteIP", "dattoSerialNumber", "dattoUptimeSeconds", "dattoUsedKilobytes", "dattoZFSVersionID", "deviceNetworkingID", "domain", "domainRegistrarID", "domainRegistrationDateTime", "domainLastUpdatedDateTime", "domainExpirationDateTime", "hourlyCost", "impersonatorCreatorResourceID", "installDate", "installedByContactID", "installedByID", "isActive", "lastActivityPersonID", "lastActivityPersonType", "lastModifiedTime", "location", "monthlyCost", "notes", "numberOfUsers", "parentConfigurationItemID", "perUseCost", "productID", "referenceNumber", "referenceTitle", "rmmDeviceAuditAntivirusStatusID", "rmmDeviceAuditArchitectureID", "rmmDeviceAuditBackupStatusID", "rmmDeviceAuditDescription", "rmmDeviceAuditDeviceTypeID", "rmmDeviceAuditDisplayAdaptorID", "rmmDeviceAuditDomainID", "rmmDeviceAuditExternalIPAddress", "rmmDeviceAuditHostname", "rmmDeviceAuditIPAddress", "rmmDeviceAuditLastUser", "rmmDeviceAuditMacAddress", "rmmDeviceAuditManufacturerID", "rmmDeviceAuditFirmwareID", "rmmDeviceAuditMemoryBytes", "rmmDeviceAuditMissingPatchCount", "rmmDeviceAuditMobileNetworkOperatorID", "rmmDeviceAuditMobileNumber", "rmmDeviceAuditModelID", "rmmDeviceAuditMotherboardID", "rmmDeviceAuditOperatingSystem", "rmmDeviceAuditPatchStatusID", "rmmDeviceAuditProcessorID", "rmmDeviceAuditServicePackID", "rmmDeviceAuditSNMPContact", "rmmDeviceAuditSNMPLocation", "rmmDeviceAuditSNMPName", "rmmDeviceAuditSoftwareStatusID", "rmmDeviceAuditStorageBytes", "rmmDeviceID", "rmmDeviceUID", "rmmVsaxDeviceID", "rmmOpenAlertCount", "serialNumber", "serviceBundleID", "serviceID", "serviceLevelAgreementID", "setupFee", "sourceChargeID", "sourceChargeType", "sslSource", "sslCommonName", "sslValidFromDateTime", "sslValidUntilDateTime", "sslIssuedBy", "sslOrganization", "sslOrganizationUnit", "sslLocation", "sslSerialNumber", "sslSignatureAlgorithm", "vendorID", "warrantyExpirationDate", "rmmIsInMaintenanceMode", "rmmIsMobileDeviceManagementEnrolled", "rmmDeviceUrl", "userDefinedFields"],
18227
+ "contact_group_contacts": ["id", "contactId", "contactGroupId", "soapParentPropertyId"],
18228
+ "contact_groups": ["id", "name", "isActive"],
18229
+ "contact_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
18230
+ "contacts": ["id", "additionalAddressInformation", "addressLine", "addressLine1", "alternatePhone", "apiVendorID", "bulkEmailOptOutTime", "city", "companyID", "companyLocationID", "countryID", "createDate", "emailAddress", "emailAddress2", "emailAddress3", "extension", "externalID", "facebookUrl", "faxNumber", "firstName", "impersonatorCreatorResourceID", "isActive", "isOptedOutFromBulkEmail", "lastActivityDate", "lastModifiedDate", "lastName", "linkedInUrl", "middleInitial", "mobilePhone", "namePrefix", "nameSuffix", "note", "receivesEmailNotifications", "phone", "primaryContact", "roomNumber", "solicitationOptOut", "solicitationOptOutTime", "state", "surveyOptOut", "title", "twitterUrl", "zipCode", "soapParentPropertyId", "userDefinedFields"],
18231
+ "contract_billing_rules": ["id", "contractID", "createChargesAsBillable", "dailyProratedCost", "dailyProratedPrice", "determineUnits", "endDate", "executionMethod", "includeItemsInChargeDescription", "invoiceDescription", "isActive", "isDailyProrationEnabled", "maximumUnits", "minimumUnits", "productID", "startDate", "soapParentPropertyId"],
18232
+ "contract_block_hour_factors": ["id", "blockHourMultiplier", "contractHourlyRate", "contractID", "internalCurrencyContractHourlyRate", "roleID", "soapParentPropertyId"],
18233
+ "contract_blocks": ["id", "contractID", "datePurchased", "endDate", "hourlyRate", "hours", "hoursApproved", "invoiceNumber", "isPaid", "paymentNumber", "paymentType", "startDate", "status", "soapParentPropertyId"],
18234
+ "contract_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
18235
+ "contract_exclusion_billing_codes": ["id", "billingCodeID", "contractID", "soapParentPropertyId"],
18236
+ "contract_exclusion_roles": ["id", "contractID", "roleID", "soapParentPropertyId"],
18237
+ "contract_exclusion_set_excluded_roles": ["id", "contractExclusionSetID", "excludedRoleID", "soapParentPropertyId"],
18238
+ "contract_exclusion_set_excluded_work_types": ["id", "contractExclusionSetID", "excludedWorkTypeID", "soapParentPropertyId"],
18239
+ "contract_exclusion_sets": ["id", "description", "isActive", "name"],
18240
+ "contract_milestones": ["id", "amount", "billingCodeID", "contractID", "createDate", "creatorResourceID", "dateDue", "description", "internalCurrencyAmount", "isInitialPayment", "organizationalLevelAssociationID", "status", "title", "soapParentPropertyId"],
18241
+ "contract_notes": ["id", "contractID", "createDateTime", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "title", "soapParentPropertyId"],
18242
+ "contract_rates": ["id", "contractHourlyRate", "contractID", "internalCurrencyContractHourlyRate", "roleID", "soapParentPropertyId"],
18243
+ "contract_retainers": ["id", "amount", "amountApproved", "contractID", "datePurchased", "endDate", "internalCurrencyAmount", "internalCurrencyAmountApproved", "invoiceNumber", "isPaid", "paymentID", "paymentNumber", "startDate", "status", "soapParentPropertyId"],
18244
+ "contract_role_costs": ["id", "contractID", "rate", "resourceID", "roleID", "soapParentPropertyId"],
18245
+ "contract_service_bundle_units": ["id", "approveAndPostDate", "contractID", "contractServiceBundleID", "cost", "endDate", "internalCurrencyPrice", "organizationalLevelAssociationID", "price", "serviceBundleID", "startDate", "units", "soapParentPropertyId"],
18246
+ "contract_service_bundles": ["id", "adjustedPrice", "contractID", "internalCurrencyAdjustedPrice", "internalCurrencyUnitPrice", "internalDescription", "invoiceDescription", "quoteItemID", "serviceBundleID", "unitPrice", "soapParentPropertyId"],
18247
+ "contract_service_units": ["id", "approveAndPostDate", "contractID", "contractServiceID", "cost", "endDate", "internalCurrencyPrice", "organizationalLevelAssociationID", "price", "serviceID", "startDate", "units", "vendorCompanyID", "soapParentPropertyId"],
18248
+ "contract_services": ["id", "contractID", "internalCurrencyAdjustedPrice", "internalCurrencyUnitPrice", "internalDescription", "invoiceDescription", "quoteItemID", "serviceID", "unitCost", "unitPrice", "soapParentPropertyId"],
18249
+ "contract_ticket_purchases": ["id", "contractID", "datePurchased", "endDate", "invoiceNumber", "isPaid", "paymentNumber", "paymentType", "perTicketRate", "startDate", "status", "ticketsPurchased", "ticketsUsed", "soapParentPropertyId"],
18250
+ "contracts": ["id", "billingPreference", "billToCompanyContactID", "billToCompanyID", "companyID", "contactID", "contactName", "contractCategory", "contractExclusionSetID", "contractName", "contractNumber", "contractPeriodType", "contractType", "description", "endDate", "estimatedCost", "estimatedHours", "estimatedRevenue", "exclusionContractID", "internalCurrencyOverageBillingRate", "internalCurrencySetupFee", "isCompliant", "isDefaultContract", "lastModifiedDateTime", "opportunityID", "organizationalLevelAssociationID", "overageBillingRate", "purchaseOrderNumber", "renewedContractID", "serviceLevelAgreementID", "setupFee", "setupFeeBillingCodeID", "startDate", "status", "timeReportingRequiresStartAndStopTimes", "userDefinedFields"],
18251
+ "countries": ["id", "addressFormatID", "countryCode", "displayName", "invoiceTemplateID", "isActive", "isDefaultCountry", "name", "purchaseOrderTemplateID", "quoteTemplateID"],
18252
+ "currencies": ["id", "currencyNegativeFormat", "currencyPositiveFormat", "description", "displaySymbol", "exchangeRate", "isActive", "isInternalCurrency", "lastModifiedDateTime", "name", "updateResourceId"],
18253
+ "departments": ["id", "description", "name", "number", "primaryLocationID"],
18254
+ "domain_registrars": ["id", "name", "url"],
18255
+ "expense_item_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "expenseItemID", "expenseReportID", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
18256
+ "expense_items": ["id", "companyID", "description", "destination", "entertainmentLocation", "expenseCategory", "expenseCurrencyExpenseAmount", "expenseCurrencyID", "expenseDate", "expenseReportID", "glCode", "haveReceipt", "internalCurrencyExpenseAmount", "internalCurrencyReimbursementAmount", "isBillableToCompany", "isReimbursable", "isRejected", "miles", "odometerEnd", "odometerStart", "origin", "paymentType", "projectID", "purchaseOrderNumber", "reimbursementCurrencyReimbursementAmount", "taskID", "ticketID", "workType", "soapParentPropertyId"],
18257
+ "expense_reports": ["id", "amountDue", "approvedDate", "approverID", "departmentNumber", "internalCurrencyCashAdvanceAmount", "internalCurrencyExpenseTotal", "name", "organizationalLevelAssociationID", "quickBooksReferenceNumber", "reimbursementCurrencyAmountDue", "reimbursementCurrencyCashAdvanceAmount", "reimbursementCurrencyID", "rejectionReason", "status", "submit", "submitDate", "submitterID", "weekEnding"],
18258
+ "holiday_sets": ["id", "holidaySetDescription", "holidaySetName"],
18259
+ "holidays": ["id", "holidayDate", "holidayName", "holidaySetID", "soapParentPropertyId"],
18260
+ "inventory_products": ["id", "availableUnits", "backOrderQuantity", "bin", "createDateTime", "createdByResourceID", "inventoryLocationID", "onHandUnits", "pickedUnits", "productID", "quantityMaximum", "quantityMinimum", "referenceNumber", "reservedUnits", "unitsOnOrder"],
18261
+ "invoice_templates": ["id", "coveredByBlockRetainerContractLabel", "coveredByRecurringServiceFixedPricePerTicketContractLabel", "currencyNegativeFormat", "currencyPositiveFormat", "dateFormat", "displayFixedPriceContractLabor", "displayRecurringServiceContractLabor", "displaySeparateLineItemForEachTax", "displayTaxCategory", "displayTaxCategorySuperscripts", "displayZeroAmountRecurringServicesAndBundles", "groupBy", "itemizeItemsInEachGroup", "itemizeServicesAndBundles", "name", "nonBillableLaborLabel", "numberFormat", "pageLayout", "pageNumberFormat", "paymentTerms", "rateCostExpression", "showGridHeader", "showVerticalGridLines", "sortBy", "timeFormat"],
18262
+ "invoices": ["id", "batchID", "comments", "companyID", "createDateTime", "creatorResourceID", "dueDate", "fromDate", "invoiceDateTime", "invoiceEditorTemplateID", "invoiceNumber", "invoiceTotal", "isVoided", "orderNumber", "paidDate", "paymentTerm", "taxGroup", "taxRegionName", "toDate", "totalTaxValue", "voidedByResourceID", "voidedDate", "webServiceDate"],
18263
+ "notification_history": ["id", "companyID", "entityNumber", "entityTitle", "initiatingContactID", "initiatingResourceID", "isActive", "isDeleted", "isTemplateJob", "notificationHistoryTypeID", "notificationSentTime", "opportunityID", "projectID", "quoteID", "recipientDisplayName", "recipientEmailAddress", "taskID", "templateName", "ticketID", "timeEntryID"],
18264
+ "opportunities": ["id", "advancedField1", "advancedField2", "advancedField3", "advancedField4", "advancedField5", "amount", "assessmentScore", "barriers", "closedDate", "companyID", "contactID", "cost", "createDate", "creatorResourceID", "description", "helpNeeded", "impersonatorCreatorResourceID", "lastActivity", "leadSource", "lossReason", "lossReasonDetail", "lostDate", "market", "monthlyCost", "monthlyRevenue", "nextStep", "onetimeCost", "onetimeRevenue", "opportunityCategoryID", "organizationalLevelAssociationID", "ownerResourceID", "primaryCompetitor", "probability", "productID", "projectedCloseDate", "promisedFulfillmentDate", "promotionName", "quarterlyCost", "quarterlyRevenue", "rating", "relationshipAssessmentScore", "revenueSpread", "revenueSpreadUnit", "salesOrderID", "salesProcessPercentComplete", "semiannualCost", "semiannualRevenue", "stage", "startDate", "status", "technicalAssessmentScore", "throughDate", "title", "totalAmountMonths", "useQuoteTotals", "winReason", "winReasonDetail", "yearlyCost", "yearlyRevenue", "userDefinedFields"],
18265
+ "opportunity_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentAttachmentID", "parentID", "publish", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
18266
+ "opportunity_categories": ["id", "displayColorRGB", "isActive", "isGlobalDefault", "name", "nickname"],
18267
+ "organizational_level1s": ["id", "description", "isActive", "name"],
18268
+ "organizational_level2s": ["id", "description", "isActive", "name"],
18269
+ "organizational_level_associations": ["id", "isActive", "organizationalLevel1ID", "organizationalLevel2ID"],
18270
+ "phases": ["id", "createDate", "creatorResourceID", "description", "dueDate", "estimatedHours", "externalID", "isScheduled", "lastActivityDateTime", "parentPhaseID", "phaseNumber", "projectID", "startDate", "title", "soapParentPropertyId"],
18271
+ "product_vendors": ["id", "isActive", "isDefault", "productID", "vendorCost", "vendorID", "vendorPartNumber", "soapParentPropertyId"],
18272
+ "products": ["id", "billingType", "chargeBillingCodeID", "createdByResourceID", "createdTime", "defaultVendorID", "description", "doesNotRequireProcurement", "externalProductID", "impersonatorCreatorResourceID", "internalProductID", "isActive", "isEligibleForRma", "isSerialized", "link", "manufacturerName", "manufacturerProductName", "markupRate", "msrp", "name", "periodType", "priceCostMethod", "productBillingCodeID", "productCategory", "sku", "unitCost", "unitPrice", "vendorProductNumber", "defaultInstalledProductCategoryID", "userDefinedFields"],
18273
+ "project_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "estimatedCost", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "projectID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
18274
+ "project_notes": ["id", "createDateTime", "createdByContactID", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "isAnnouncement", "lastActivityDate", "noteType", "projectID", "publish", "title", "soapParentPropertyId"],
18275
+ "projects": ["id", "actualBilledHours", "actualHours", "changeOrdersBudget", "changeOrdersRevenue", "companyID", "companyOwnerResourceID", "completedDateTime", "completedPercentage", "contractID", "createDateTime", "creatorResourceID", "department", "description", "duration", "endDateTime", "estimatedSalesCost", "estimatedTime", "extProjectNumber", "extProjectType", "impersonatorCreatorResourceID", "laborEstimatedCosts", "laborEstimatedMarginPercentage", "laborEstimatedRevenue", "lastActivityDateTime", "lastActivityPersonType", "lastActivityResourceID", "opportunityID", "organizationalLevelAssociationID", "originalEstimatedRevenue", "projectCostEstimatedMarginPercentage", "projectCostsBudget", "projectCostsRevenue", "projectLeadResourceID", "projectName", "projectNumber", "projectType", "purchaseOrderNumber", "sgda", "startDateTime", "status", "statusDateTime", "statusDetail", "userDefinedFields"],
18276
+ "purchase_approvals": ["id", "costType", "isApproved", "rejectNote"],
18277
+ "purchase_order_item_receiving": ["id", "purchaseOrderItemID", "quantityBackOrdered", "quantityNowReceiving", "quantityPreviouslyReceived", "receiveDate", "receivedByResourceID", "serialNumber", "vendorInvoiceNumber", "soapParentPropertyId"],
18278
+ "purchase_order_items": ["id", "chargeID", "contractID", "estimatedArrivalDate", "internalCurrencyUnitCost", "inventoryLocationID", "memo", "orderID", "productID", "projectID", "quantity", "salesOrderID", "ticketID", "unitCost", "soapParentPropertyId"],
18279
+ "purchase_orders": ["id", "cancelDateTime", "createDateTime", "creatorResourceID", "externalPONumber", "fax", "freight", "generalMemo", "impersonatorCreatorResourceID", "internalCurrencyFreight", "latestEstimatedArrivalDate", "paymentTerm", "phone", "purchaseForCompanyID", "purchaseOrderNumber", "purchaseOrderTemplateID", "shippingDate", "shippingType", "shipToAddress1", "shipToAddress2", "shipToCity", "shipToName", "shipToPostalCode", "shipToState", "showEachTaxInGroup", "showTaxCategory", "status", "submitDateTime", "taxRegionID", "useItemDescriptionsFrom", "vendorID", "vendorInvoiceNumber", "additionalVendorInvoiceNumbers"],
18280
+ "quote_items": ["id", "averageCost", "chargeID", "description", "expenseID", "highestCost", "internalCurrencyLineDiscount", "internalCurrencyUnitDiscount", "internalCurrencyUnitPrice", "isOptional", "isTaxable", "laborID", "lineDiscount", "markupRate", "name", "percentageDiscount", "periodType", "productID", "quantity", "quoteID", "quoteItemType", "serviceBundleID", "serviceID", "shippingID", "sortOrderID", "taxCategoryID", "totalEffectiveTax", "unitCost", "unitDiscount", "unitPrice", "soapParentPropertyId"],
18281
+ "quote_locations": ["id", "address1", "address2", "city", "postalCode", "state"],
18282
+ "quote_templates": ["id", "calculateTaxSeparately", "createDate", "createdBy", "currencyNegativeFormat", "currencyPositiveFormat", "dateFormat", "description", "displayTaxCategorySuperscripts", "isActive", "lastActivityBy", "lastActivityDate", "name", "numberFormat", "pageLayout", "pageNumberFormat", "showEachTaxInGroup", "showGridHeader", "showTaxCategory", "showVerticalGridLines"],
18283
+ "quotes": ["id", "approvalStatus", "approvalStatusChangedByResourceID", "approvalStatusChangedDate", "billToLocationID", "calculateTaxSeparately", "comment", "companyID", "contactID", "createDate", "creatorResourceID", "description", "effectiveDate", "expirationDate", "extApprovalContactResponse", "extApprovalResponseDate", "extApprovalResponseSignature", "externalQuoteNumber", "groupByID", "impersonatorCreatorResourceID", "isActive", "lastActivityDate", "lastModifiedBy", "lastPublishedByResourceID", "lastPublishedDateTime", "name", "opportunityID", "paymentTerm", "paymentType", "primaryQuote", "proposalProjectID", "purchaseOrderNumber", "quoteNumber", "quoteTemplateID", "shippingType", "shipToLocationID", "showEachTaxInGroup", "showTaxCategory", "soldToLocationID", "taxRegionID"],
18284
+ "resource_role_queues": ["id", "queueID", "resourceID", "soapParentPropertyId"],
18285
+ "resource_roles": ["id", "departmentID", "isActive", "queueID", "resourceID", "roleID", "soapParentPropertyId"],
18286
+ "resource_skills": ["id", "resourceID", "skillDescription", "skillID", "skillLevel", "soapParentPropertyId"],
18287
+ "resource_time_off_approvers": ["id", "resourceID", "approverResourceID", "approvalLevel", "soapParentPropertyId"],
18288
+ "resource_time_off_balances": ["id", "resourceID", "floatingHolidayAnnualAllowance", "floatingHolidayBalance", "floatingHolidayPlanned", "floatingHolidayUsed", "floatingHolidayWaitingApproval", "personalTimeAnnualAllowance", "personalTimeBalance", "personalTimePlanned", "personalTimeUsed", "personalTimeWaitingApproval", "sickTimeAnnualAllowance", "sickTimeBalance", "sickTimePlanned", "sickTimeUsed", "sickTimeWaitingApproval", "vacationTimeAnnualAllowance", "vacationTimeBalance", "vacationTimePlanned", "vacationTimeUsed", "vacationTimeWaitingApproval", "year", "soapParentPropertyId"],
18289
+ "resources": ["id", "accountingReferenceID", "dateFormat", "defaultServiceDeskRoleID", "email", "email2", "email3", "emailTypeCode", "emailTypeCode2", "emailTypeCode3", "firstName", "gender", "greeting", "hireDate", "homePhone", "initials", "internalCost", "isActive", "lastName", "licenseType", "locationID", "middleName", "mobilePhone", "numberFormat", "officeExtension", "officePhone", "payrollIdentifier", "payrollType", "resourceType", "suffix", "surveyResourceRating", "timeFormat", "title", "travelAvailabilityPct", "userName", "userType"],
18290
+ "roles": ["id", "description", "hourlyFactor", "hourlyRate", "isActive", "isExcludedFromNewContracts", "isSystemRole", "name", "quoteItemDefaultTaxCategoryId", "roleType"],
18291
+ "sales_orders": ["id", "additionalBillToAddressInformation", "additionalShipToAddressInformation", "billingAddress1", "billingAddress2", "billToCity", "billToCountryID", "billToPostalCode", "billToState", "companyID", "contactID", "impersonatorCreatorResourceID", "opportunityID", "organizationalLevelAssociationID", "ownerResourceID", "promisedFulfillmentDate", "salesOrderDate", "shipToAddress1", "shipToAddress2", "shipToCity", "shipToCountryID", "shipToPostalCode", "shipToState", "status", "title", "soapParentPropertyId", "userDefinedFields"],
18292
+ "service_call_task_resources": ["id", "resourceID", "serviceCallTaskID", "soapParentPropertyId"],
18293
+ "service_call_tasks": ["id", "serviceCallID", "taskID", "soapParentPropertyId"],
18294
+ "service_call_ticket_resources": ["id", "resourceID", "serviceCallTicketID", "soapParentPropertyId"],
18295
+ "service_call_tickets": ["id", "serviceCallID", "ticketID", "soapParentPropertyId"],
18296
+ "service_calls": ["id", "cancelationNoticeHours", "canceledByResourceID", "canceledDateTime", "companyID", "companyLocationID", "createDateTime", "creatorResourceID", "description", "duration", "endDateTime", "impersonatorCreatorResourceID", "isComplete", "lastModifiedDateTime", "startDateTime", "status"],
18297
+ "service_level_agreement_results": ["id", "firstResponseElapsedHours", "firstResponseInitiatingResourceID", "firstResponseResourceID", "isFirstResponseMet", "isResolutionMet", "isResolutionPlanMet", "resolutionElapsedHours", "resolutionPlanElapsedHours", "resolutionPlanResourceID", "resolutionResourceID", "serviceLevelAgreementName", "ticketID", "soapParentPropertyId"],
18298
+ "services": ["id", "billingCodeID", "createDate", "creatorResourceID", "description", "invoiceDescription", "isActive", "lastModifiedDate", "markupRate", "name", "periodType", "serviceLevelAgreementID", "unitCost", "unitPrice", "updateResourceID", "vendorCompanyID", "manufacturerServiceProvider", "manufacturerServiceProviderProductNumber", "catalogNumberPartNumber", "sku", "internalID", "externalID", "url", "userDefinedFields"],
18299
+ "skills": ["id", "categoryID", "description", "isActive", "name"],
18300
+ "subscription_periods": ["id", "periodCost", "periodDate", "periodPrice", "postedDate", "purchaseOrderNumber", "subscriptionID", "soapParentPropertyId"],
18301
+ "subscriptions": ["id", "configurationItemID", "description", "effectiveDate", "expirationDate", "impersonatorCreatorResourceID", "materialCodeID", "organizationalLevelAssociationID", "periodCost", "periodPrice", "periodType", "purchaseOrderNumber", "status", "subscriptionName", "totalCost", "totalPrice", "vendorID", "userDefinedFields"],
18302
+ "survey_results": ["id", "companyID", "companyRating", "completeDate", "contactID", "contactRating", "resourceRating", "sendDate", "surveyID", "surveyRating", "ticketID"],
18303
+ "surveys": ["id", "description", "displayName", "name"],
18304
+ "tag_aliases": ["id", "alias", "tagID", "soapParentPropertyId"],
18305
+ "tag_groups": ["id", "displayColor", "isActive", "isSystem", "label"],
18306
+ "tags": ["id", "createDateTime", "isActive", "isExcludedFromAutomaticTagging", "isSystem", "label", "lastModifiedDateTime", "tagGroupID"],
18307
+ "task_secondary_resources": ["id", "resourceID", "roleID", "taskID", "soapParentPropertyId"],
18308
+ "tasks": ["id", "assignedResourceID", "assignedResourceRoleID", "billingCodeID", "canClientPortalUserCompleteTask", "companyLocationID", "completedByResourceID", "completedByType", "completedDateTime", "createDateTime", "creatorResourceID", "creatorType", "departmentID", "description", "endDateTime", "estimatedHours", "externalID", "hoursToBeScheduled", "isTaskBillable", "isVisibleInClientPortal", "lastActivityDateTime", "lastActivityPersonType", "lastActivityResourceID", "phaseID", "priority", "priorityLabel", "projectID", "purchaseOrderNumber", "remainingHours", "startDateTime", "status", "taskCategoryID", "taskNumber", "taskType", "title", "soapParentPropertyId", "userDefinedFields"],
18309
+ "tax_regions": ["id", "isActive", "name"],
18310
+ "ticket_additional_configuration_items": ["id", "configurationItemID", "ticketID", "soapParentPropertyId"],
18311
+ "ticket_additional_contacts": ["id", "contactID", "ticketID", "soapParentPropertyId"],
18312
+ "ticket_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentAttachmentID", "parentID", "publish", "ticketID", "ticketNoteID", "timeEntryID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
18313
+ "ticket_categories": ["id", "displayColorRGB", "isActive", "isApiOnly", "isGlobalDefault", "name", "nickname"],
18314
+ "ticket_category_field_defaults": ["id", "description", "estimatedHours", "issueTypeID", "organizationalLevelAssociationID", "priority", "purchaseOrderNumber", "queueID", "resolution", "serviceLevelAgreementID", "sourceID", "status", "subIssueTypeID", "ticketCategoryID", "ticketTypeID", "title", "workTypeID", "soapParentPropertyId"],
18315
+ "ticket_change_request_approvals": ["id", "approveRejectDateTime", "approveRejectNote", "contactID", "isApproved", "resourceID", "ticketID", "soapParentPropertyId"],
18316
+ "ticket_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "ticketID", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
18317
+ "ticket_checklist_items": ["id", "completedByResourceID", "completedDateTime", "isCompleted", "isImportant", "itemName", "knowledgebaseArticleID", "position", "ticketID", "soapParentPropertyId"],
18318
+ "ticket_history": ["id", "action", "date", "detail", "resourceID", "ticketID", "soapParentPropertyId"],
18319
+ "ticket_note_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "ticketID", "ticketNoteID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
18320
+ "ticket_note_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
18321
+ "ticket_notes": ["id", "createDateTime", "createdByContactID", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "noteType", "publish", "ticketID", "title", "soapParentPropertyId"],
18322
+ "ticket_secondary_resources": ["id", "resourceID", "roleID", "ticketID", "soapParentPropertyId"],
18323
+ "ticket_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
18324
+ "tickets": ["id", "apiVendorID", "assignedResourceID", "assignedResourceRoleID", "billingCodeID", "changeApprovalBoard", "changeApprovalStatus", "changeApprovalType", "changeInfoField1", "changeInfoField2", "changeInfoField3", "changeInfoField4", "changeInfoField5", "companyID", "companyLocationID", "completedByResourceID", "completedDate", "configurationItemID", "contactID", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "createdByContactID", "creatorResourceID", "creatorType", "currentServiceThermometerRating", "description", "dueDateTime", "estimatedHours", "externalID", "firstResponseAssignedResourceID", "firstResponseDateTime", "firstResponseDueDateTime", "firstResponseInitiatingResourceID", "hoursToBeScheduled", "impersonatorCreatorResourceID", "isAssignedToComanaged", "issueType", "isVisibleToComanaged", "lastActivityDate", "lastActivityPersonType", "lastActivityResourceID", "lastCustomerNotificationDateTime", "lastCustomerVisibleActivityDateTime", "lastTrackedModificationDateTime", "monitorID", "monitorTypeID", "opportunityID", "organizationalLevelAssociationID", "previousServiceThermometerRating", "priority", "problemTicketId", "projectID", "purchaseOrderNumber", "queueID", "resolution", "resolutionPlanDateTime", "resolutionPlanDueDateTime", "resolvedDateTime", "resolvedDueDateTime", "rmaStatus", "rmaType", "rmmAlertID", "serviceLevelAgreementHasBeenMet", "serviceLevelAgreementID", "serviceLevelAgreementPausedNextEventHours", "serviceThermometerTemperature", "source", "status", "subIssueType", "ticketCategory", "ticketNumber", "ticketType", "title", "userDefinedFields"],
18325
+ "time_entries": ["id", "billingApprovalDateTime", "billingApprovalLevelMostRecent", "billingApprovalResourceID", "billingCodeID", "contractID", "contractServiceBundleID", "contractServiceID", "createDateTime", "creatorUserID", "dateWorked", "endDateTime", "hoursToBill", "hoursWorked", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "internalBillingCodeID", "internalNotes", "isInternalNotesVisibleToComanaged", "isNonBillable", "lastModifiedDateTime", "lastModifiedUserID", "offsetHours", "resourceID", "roleID", "showOnInvoice", "startDateTime", "summaryNotes", "taskID", "ticketID", "timeEntryType"],
18326
+ "time_entry_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "taskID", "ticketID", "timeEntryID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
18327
+ "time_off_requests": ["id", "approvedDateTime", "approveRejectResourceID", "createdByResourceID", "createDateTime", "endTime", "hours", "impersonatorApproveRejectResourceID", "lastModifiedByResourceID", "lastModifiedDateTime", "lastApprovedLevel", "reason", "rejectReason", "requestDate", "resourceID", "startTime", "status", "timeOffRequestType", "soapParentPropertyId"]
18328
+ };
18329
+ var MODEL_DATE_FIELDS = {
18330
+ "appointments": ["createDateTime", "endDateTime", "startDateTime", "updateDateTime"],
18331
+ "billing_item_approval_levels": ["approvalDateTime"],
18332
+ "billing_items": ["itemDate", "postedDate", "postedOnTime", "webServiceDate"],
18333
+ "companies": ["createDate", "lastActivityDate", "lastTrackedModifiedDateTime"],
18334
+ "company_notes": ["completedDateTime", "createDateTime", "endDateTime", "lastModifiedDate", "startDateTime"],
18335
+ "configuration_item_billing_product_associations": ["effectiveDate", "expirationDate"],
18336
+ "configuration_item_dns_records": ["createDateTime"],
18337
+ "configuration_item_notes": ["createDateTime", "lastActivityDate"],
18338
+ "configuration_items": ["createDate", "dattoLastCheckInDateTime", "domainRegistrationDateTime", "domainLastUpdatedDateTime", "domainExpirationDateTime", "installDate", "lastModifiedTime", "sslValidFromDateTime", "sslValidUntilDateTime", "warrantyExpirationDate"],
18339
+ "contacts": ["bulkEmailOptOutTime", "createDate", "lastActivityDate", "lastModifiedDate", "solicitationOptOutTime"],
18340
+ "contract_billing_rules": ["endDate", "startDate"],
18341
+ "contract_blocks": ["datePurchased", "endDate", "startDate"],
18342
+ "contract_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
18343
+ "contract_milestones": ["createDate", "dateDue"],
18344
+ "contract_notes": ["createDateTime", "lastActivityDate"],
18345
+ "contract_retainers": ["datePurchased", "endDate", "startDate"],
18346
+ "contract_service_bundle_units": ["approveAndPostDate", "endDate", "startDate"],
18347
+ "contract_service_units": ["approveAndPostDate", "endDate", "startDate"],
18348
+ "contract_ticket_purchases": ["datePurchased", "endDate", "startDate"],
18349
+ "contracts": ["endDate", "lastModifiedDateTime", "startDate"],
18350
+ "currencies": ["lastModifiedDateTime"],
18351
+ "expense_item_attachments": ["attachDate"],
18352
+ "expense_items": ["expenseDate"],
18353
+ "expense_reports": ["approvedDate", "submitDate", "weekEnding"],
18354
+ "holidays": ["holidayDate"],
18355
+ "inventory_products": ["createDateTime"],
18356
+ "invoices": ["createDateTime", "dueDate", "fromDate", "invoiceDateTime", "paidDate", "toDate", "voidedDate", "webServiceDate"],
18357
+ "notification_history": ["notificationSentTime"],
18358
+ "opportunities": ["closedDate", "createDate", "lastActivity", "lostDate", "projectedCloseDate", "promisedFulfillmentDate", "startDate", "throughDate"],
18359
+ "opportunity_attachments": ["attachDate"],
18360
+ "phases": ["createDate", "dueDate", "lastActivityDateTime", "startDate"],
18361
+ "products": ["createdTime"],
18362
+ "project_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
18363
+ "project_notes": ["createDateTime", "lastActivityDate"],
18364
+ "projects": ["completedDateTime", "createDateTime", "endDateTime", "lastActivityDateTime", "startDateTime", "statusDateTime"],
18365
+ "purchase_order_item_receiving": ["receiveDate"],
18366
+ "purchase_order_items": ["estimatedArrivalDate"],
18367
+ "purchase_orders": ["cancelDateTime", "createDateTime", "latestEstimatedArrivalDate", "shippingDate", "submitDateTime"],
18368
+ "quote_templates": ["createDate", "lastActivityDate"],
18369
+ "quotes": ["approvalStatusChangedDate", "createDate", "effectiveDate", "expirationDate", "extApprovalResponseDate", "lastActivityDate", "lastPublishedDateTime"],
18370
+ "resources": ["hireDate"],
18371
+ "sales_orders": ["promisedFulfillmentDate", "salesOrderDate"],
18372
+ "service_calls": ["canceledDateTime", "createDateTime", "endDateTime", "lastModifiedDateTime", "startDateTime"],
18373
+ "services": ["createDate", "lastModifiedDate"],
18374
+ "subscription_periods": ["periodDate", "postedDate"],
18375
+ "subscriptions": ["effectiveDate", "expirationDate"],
18376
+ "survey_results": ["completeDate", "sendDate"],
18377
+ "tags": ["createDateTime", "lastModifiedDateTime"],
18378
+ "tasks": ["completedDateTime", "createDateTime", "endDateTime", "lastActivityDateTime", "startDateTime"],
18379
+ "ticket_attachments": ["attachDate"],
18380
+ "ticket_change_request_approvals": ["approveRejectDateTime"],
18381
+ "ticket_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
18382
+ "ticket_checklist_items": ["completedDateTime"],
18383
+ "ticket_history": ["date"],
18384
+ "ticket_note_attachments": ["attachDate"],
18385
+ "ticket_notes": ["createDateTime", "lastActivityDate"],
18386
+ "tickets": ["completedDate", "createDate", "dueDateTime", "firstResponseDateTime", "firstResponseDueDateTime", "lastActivityDate", "lastCustomerNotificationDateTime", "lastCustomerVisibleActivityDateTime", "lastTrackedModificationDateTime", "resolutionPlanDateTime", "resolutionPlanDueDateTime", "resolvedDateTime", "resolvedDueDateTime"],
18387
+ "time_entries": ["billingApprovalDateTime", "createDateTime", "dateWorked", "endDateTime", "lastModifiedDateTime", "startDateTime"],
18388
+ "time_entry_attachments": ["attachDate"],
18389
+ "time_off_requests": ["approvedDateTime", "createDateTime", "endTime", "lastModifiedDateTime", "requestDate", "startTime"]
18390
+ };
18391
+
18392
+ // src/resources/base/types.ts
18393
+ var BOUNDED_QUERY_DEFAULT_LIMIT = 500;
18394
+ var BOUNDED_QUERY_MAX_LIMIT = 2500;
18395
+ var LATEST_MAX_COUNT_PROBES = 64;
18396
+ var MARKER_RECOVERY_LIMIT = 5;
18397
+
18398
+ // src/resources/base/helpers.ts
18399
+ function describeResponseShape(body) {
18400
+ if (body === null) return "null";
18401
+ if (Array.isArray(body)) return `array(len=${body.length})`;
18402
+ if (typeof body !== "object") return typeof body;
18403
+ const rec = body;
18404
+ const keys = Object.keys(rec).sort();
18405
+ const typeOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
18406
+ const parts = keys.slice(0, 16).map((k) => `${k}:${typeOf(rec[k])}`);
18407
+ const more = keys.length > 16 ? `, +${keys.length - 16} more` : "";
18408
+ return `{ ${parts.join(", ")}${more} }`;
18409
+ }
18410
+ function defaultMarkerToken(data) {
18411
+ for (const field of ["name", "title"]) {
18412
+ const v = data[field];
18413
+ if (typeof v === "string" && v.trim() !== "") return v.trim();
18414
+ }
18415
+ return null;
18416
+ }
18417
+ var CONTEXT_ABSENT_MARKER = "#context-absent";
18418
+ function contextAbsentKey(path) {
18419
+ return path + "\0context-absent";
18420
+ }
18421
+ function noteCharsTruncationMarker(charsRemoved) {
18422
+ return `\u2026 [+${charsRemoved} chars truncated]`;
18423
+ }
18424
+ function truncateNoteChars(items, cap, fields) {
18425
+ let truncated = 0;
18426
+ const out = items.map((item) => {
18427
+ if (!isRecord2(item)) return item;
18428
+ let changed = false;
18429
+ const copy = { ...item };
18430
+ for (const field of fields) {
18431
+ const v = copy[field];
18432
+ if (typeof v === "string" && v.length > cap) {
18433
+ const end = cap > 0 && (v.charCodeAt(cap - 1) & 64512) === 55296 ? cap - 1 : cap;
18434
+ copy[field] = v.slice(0, end) + noteCharsTruncationMarker(v.length - end);
18435
+ truncated += 1;
18436
+ changed = true;
18437
+ }
18438
+ }
18439
+ return changed ? copy : item;
18440
+ });
18441
+ return [out, truncated];
18442
+ }
18443
+ function blindEmptyReason(stamp) {
18444
+ return {
18445
+ surface: stamp.surface,
18446
+ kind: "blind-surface",
18447
+ note: stamp.note,
18448
+ authority: stamp.authority,
18449
+ remedy: "the note is the remedy \u2014 the authority op answers for a record"
18450
+ };
18451
+ }
18452
+ function attachUpdateTargetMissReason(err, key) {
18453
+ const pinned = EMPTY_REASON_STAMPS[key]?.update;
18454
+ if (pinned === void 0) return;
18455
+ err.details = { ...err.details ?? {}, emptyReason: pinned };
18456
+ }
18457
+ function buildDryRunResult(spec, policy = { redactPersonalData: true }) {
18458
+ if (spec.operation === "") {
18459
+ throw new AutotaskConfigError("dry-run: the operation name is required");
18460
+ }
18461
+ const wouldApply = spec.checks.every((check) => check.ok);
18462
+ const resource = spec.operation.split(".")[0] ?? spec.operation;
18463
+ const diffPart = spec.diff !== void 0 ? {
18464
+ diff: spec.diff.map((entry) => {
18465
+ if (entry.to === void 0) return entry;
18466
+ const to = isRedactedFieldName(entry.field, policy) ? REDACTED : redactResponseBody(entry.to, policy);
18467
+ return { ...entry, to };
18468
+ })
18469
+ } : {};
18470
+ const dataPart = spec.data !== void 0 ? { data: redactResponseBody(spec.data, policy) } : {};
18471
+ return {
18472
+ operation: spec.operation,
18473
+ wouldApply,
18474
+ target: { resource, ids: spec.ids },
18475
+ request: { method: spec.method, path: spec.path },
18476
+ ...diffPart,
18477
+ checks: spec.checks,
18478
+ impact: spec.impact,
18479
+ simulated: true,
18480
+ warnings: spec.warnings ?? [],
18481
+ ...dataPart,
18482
+ ...spec.emptyReason !== void 0 ? { emptyReason: spec.emptyReason } : {}
18483
+ };
18484
+ }
18485
+ function computeFieldDiff(current, data) {
18486
+ const out = [];
18487
+ for (const [field, to] of Object.entries(data)) {
18488
+ if (field === "id" || to === void 0) continue;
18489
+ const from = current[field];
18490
+ if (from !== void 0 && JSON.stringify(from) === JSON.stringify(to)) continue;
18491
+ out.push({ field, from: from === void 0 ? null : from, to });
18492
+ }
18493
+ return out;
18494
+ }
18495
+ function mergePartialBody(current, data) {
18496
+ const merged = { ...current };
18497
+ for (const [key, value] of Object.entries(data)) {
18498
+ if (value === void 0) continue;
18499
+ merged[key] = value;
18500
+ }
18501
+ return merged;
18502
+ }
18503
+ function normalizeUdfInput(value) {
18504
+ if (Array.isArray(value)) {
18505
+ return value.map((entry) => {
18506
+ if (!isRecord2(entry) || typeof entry.name !== "string" || entry.name === "") {
18507
+ throw new AutotaskConfigError("userDefinedFields array entries must be { name, value } objects");
18508
+ }
18509
+ return { name: entry.name, value: entry.value };
18510
+ });
18511
+ }
18512
+ if (isRecord2(value)) {
18513
+ return Object.entries(value).map(([name, entryValue]) => ({ name, value: entryValue }));
18514
+ }
18515
+ throw new AutotaskConfigError("userDefinedFields must be an array of { name, value } entries or a name-to-value map");
18516
+ }
18517
+ function blindStampFor(stamp, filter) {
18518
+ if (stamp === void 0) return void 0;
18519
+ if (stamp.kind !== "unfiltered-blind") return stamp;
18520
+ const conditions = normalizedConditions(filter);
18521
+ return conditions === void 0 || conditions.length === 0 ? stamp : void 0;
18522
+ }
18523
+ function decodeFileData(body) {
18524
+ if (isRecord2(body)) {
18525
+ const data = body.data;
18526
+ if (typeof data === "string") return Buffer.from(data, "base64");
18527
+ if (Array.isArray(data) && data.every((b) => typeof b === "number")) {
18528
+ return Buffer.from(data);
18529
+ }
18530
+ }
18531
+ if (typeof body === "string") return Buffer.from(body, "base64");
18532
+ throw new AutotaskError(
18533
+ "Expected the file envelope FileQueryResultModel { data } (or a base64 string); got a different shape",
18534
+ { code: "INVALID_RESPONSE", category: "server" }
18535
+ );
18536
+ }
18537
+ var DEPLURAL_EXCEPTIONS = {
18538
+ tag_aliase: "tag_alias"
18539
+ };
18540
+ function entityNameFromPath(resourcePath) {
18541
+ const segment = resourcePath.slice(resourcePath.lastIndexOf("/") + 1);
18542
+ const snake = segment.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `_` : "") + c.toLowerCase()).replace(/_{2,}/g, "_");
18543
+ if (snake.endsWith("ies")) return `${snake.slice(0, -3)}y`;
18544
+ if (snake.endsWith("s")) {
18545
+ const stripped = snake.slice(0, -1);
18546
+ return DEPLURAL_EXCEPTIONS[stripped] ?? stripped;
18547
+ }
18548
+ return snake;
18549
+ }
18550
+
18551
+ // src/resources/special/_shared/bounded-part.ts
18552
+ async function boundedPart(http, path, conditions, limit, operation, includeFields) {
18553
+ const body = { MaxRecords: limit, filter: conditions };
18554
+ if (includeFields !== void 0) body.IncludeFields = includeFields;
18555
+ const res = await http.request({
18556
+ method: "POST",
18557
+ idempotent: true,
18558
+ path,
18559
+ body,
18560
+ operation
18561
+ });
18562
+ const { items, pageDetails } = unwrapItems(res, operation);
18563
+ const capped = items.length >= limit;
18564
+ return { items, capped, hasMore: capped && pageDetails.nextPageUrl != null };
18565
+ }
18566
+
18567
+ // src/resources/special/_shared/relation-filters.ts
18568
+ var JOIN_SET_CAP = 300;
18569
+ function capJoinSet(ids2, cap = JOIN_SET_CAP) {
18570
+ const sorted = [...new Set(ids2)].sort((a, b) => a - b);
18571
+ if (sorted.length <= cap) return { ids: sorted, truncated: false };
18572
+ return { ids: sorted.slice(0, cap), truncated: true };
18573
+ }
18574
+ var RELATION_TARGETS = {
18575
+ ticket: { resource: "tickets", path: "/V1.0/Tickets/query" },
18576
+ task: { resource: "tasks", path: "/V1.0/Tasks/query" },
18577
+ project: { resource: "projects", path: "/V1.0/Projects/query" }
18578
+ };
18579
+ function relationsFor(resourceKey) {
18580
+ const fields = MODEL_FIELD_SETS[resourceKey] ?? [];
18581
+ return Object.keys(RELATION_TARGETS).filter((rel) => fields.includes(`${rel}ID`));
18582
+ }
18583
+ var isRelationLeaf = (c) => isRecord2(c) && c.op !== "and" && c.op !== "or" && typeof c.field === "string" && c.field.includes(".");
18584
+ function hasRelationConditions(conditions) {
18585
+ if (conditions === void 0) return false;
18586
+ return conditions.some((c) => isRelationLeaf(c) || isRecord2(c) && Array.isArray(c.items) && hasRelationConditions(c.items));
18587
+ }
18588
+ function validateRelationLeaves(resourceKey, conditions, op) {
18589
+ const valid = relationsFor(resourceKey);
18590
+ const walk = (conds) => {
18591
+ for (const c of conds) {
18592
+ if (isRecord2(c) && Array.isArray(c.items)) walk(c.items);
18593
+ if (!isRelationLeaf(c)) continue;
18594
+ const field = c.field;
18595
+ const [rel, sub, ...rest] = field.split(".");
18596
+ const fail = (why) => {
18597
+ throw new AutotaskConfigError(`${op}: relation filter "${field}" ${why} \u2014 the request was not issued`, {
18598
+ operation: op,
18599
+ 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.`
18600
+ });
18601
+ };
18602
+ if (c.udf === true) fail("cannot be a UDF condition");
18603
+ if (rest.length > 0 || !sub) fail("is not <relation>.<field> (multi-hop relations are not supported)");
18604
+ if (!valid.includes(rel)) fail(`names an unknown relation for ${resourceKey} (valid: ${valid.join(", ") || "none"})`);
18605
+ const parentFields = MODEL_FIELD_SETS[RELATION_TARGETS[rel].resource];
18606
+ if (parentFields !== void 0 && !parentFields.includes(sub)) {
18607
+ fail(`names a field unknown on ${RELATION_TARGETS[rel].resource}`);
18608
+ }
18609
+ }
18610
+ };
18611
+ walk(conditions);
18612
+ }
18613
+ async function rewriteRelationFilters(http, resourceKey, conditions, op) {
18614
+ if (!hasRelationConditions(conditions)) return void 0;
18615
+ const conds = conditions;
18616
+ validateRelationLeaves(resourceKey, conds, op);
18617
+ const parentIds = async (rel, leaves) => {
18618
+ const target = RELATION_TARGETS[rel];
18619
+ const parentConds = buildFilter(leaves.map((l) => ({ ...l, field: l.field.slice(rel.length + 1) }))).filter;
18620
+ const res = await boundedPart(http, target.path, parentConds, JOIN_SET_CAP, op, ["id"]);
18621
+ if (res.hasMore) {
18622
+ throw new AutotaskConfigError(
18623
+ `${op}: the ${rel}.* conditions match more than ${JOIN_SET_CAP} ${target.resource} \u2014 the ${resourceKey} query was not issued (a partial ${rel} set would give partial results)`,
18624
+ { operation: op, suggestedAction: `Narrow the ${rel}.* conditions (e.g. add a ${rel} date or status condition) or filter ${resourceKey} by ${rel}ID directly.` }
18625
+ );
18626
+ }
18627
+ return capJoinSet(res.items.map((r) => Number(r.id)).filter((n) => Number.isFinite(n) && n > 0)).ids;
18628
+ };
18629
+ const NEVER = null;
18630
+ const rewrite = async (list, mode) => {
18631
+ const out = [];
18632
+ const byRel = /* @__PURE__ */ new Map();
18633
+ for (const c of list) {
18634
+ if (isRelationLeaf(c)) {
18635
+ const rel = c.field.split(".")[0];
18636
+ if (mode === "and") {
18637
+ byRel.set(rel, [...byRel.get(rel) ?? [], c]);
18638
+ continue;
18639
+ }
18640
+ const ids2 = await parentIds(rel, [c]);
18641
+ if (ids2.length > 0) out.push({ field: `${rel}ID`, op: "in", value: ids2 });
18642
+ continue;
18643
+ }
18644
+ if (isRecord2(c) && (c.op === "and" || c.op === "or") && Array.isArray(c.items) && hasRelationConditions(c.items)) {
18645
+ const items = await rewrite(c.items, c.op);
18646
+ if (items === NEVER) {
18647
+ if (mode === "and") return NEVER;
18648
+ continue;
18649
+ }
18650
+ out.push({ ...c, items });
18651
+ continue;
18652
+ }
18653
+ out.push(c);
18654
+ }
18655
+ for (const [rel, leaves] of byRel) {
18656
+ const ids2 = await parentIds(rel, leaves);
18657
+ if (ids2.length === 0) return NEVER;
18658
+ out.push({ field: `${rel}ID`, op: "in", value: ids2 });
18659
+ }
18660
+ return out.length === 0 && mode === "or" ? NEVER : out;
18661
+ };
18662
+ const rewritten = await rewrite(conds, "and");
18663
+ if (rewritten === NEVER) {
18664
+ return { never: true, note: `no parent record matched the relation conditions \u2014 0 ${resourceKey} rows (the ${resourceKey} query was not issued)` };
18665
+ }
18666
+ return { never: false, conditions: rewritten };
18667
+ }
18668
+
18669
+ // src/resources/base/query.ts
18670
+ var QueryBase = class {
18671
+ http;
18672
+ config;
18673
+ /** Default page size for query walks (the client's resolved `pageSize`). */
18674
+ defaultPageSize;
18675
+ /** Versioned global URL segment (from the config), e.g. '/V1.0/Companies'. */
18676
+ get resourcePath() {
18677
+ return this.config.resourcePath;
18678
+ }
18679
+ /** The vendor entity this resource's ids identify (issue #116 — the `details.idRefs` label, e.g. `company`). */
18680
+ get entityName() {
18681
+ return entityNameFromPath(this.resourcePath);
18682
+ }
18683
+ /** Parent-scoped template (from the config); undefined for top-level resources. */
18684
+ get parentScopedPath() {
18685
+ return this.config.parentScopedPath;
18686
+ }
18687
+ /** Parent field used to narrow the global query (from the config). */
18688
+ get parentField() {
18689
+ return this.config.parentField;
18690
+ }
18691
+ /** The bounded client-scan caps (section 19.2; resolved per client). */
18692
+ get resolutionCaps() {
18693
+ return this.http.resolution;
18694
+ }
18695
+ constructor(http, config, pageSize) {
18696
+ if (config.key === "" || config.resourcePath === "") {
18697
+ throw new AutotaskConfigError("BaseResource requires a non-empty key and resourcePath");
18698
+ }
18699
+ this.http = http;
18700
+ this.config = config;
18701
+ this.defaultPageSize = clampPageSize(pageSize ?? DEFAULT_PAGE_SIZE);
18702
+ }
18703
+ // ------------------------------------------------------------------
18704
+ // Query walk (wired to the G3 walker; cursor-follow per the pinned wire)
18705
+ // ------------------------------------------------------------------
18706
+ /** One page fetch: page 1 POSTs the QueryModel body; later pages POST the same body (`buildQueryBody`) to the absolute cursor (issue #118). */
18707
+ async fetchPage(params, page, pageSize, state) {
18708
+ const operation = `${this.config.key}.query`;
18709
+ if (page === 1) {
18710
+ const req = buildQueryRequest(this.http.baseUrl, this.resourcePath, { ...params, maxRecords: pageSize });
18711
+ const body2 = await this.http.request({ ...req, operation });
18712
+ return this.readPage(body2, page, pageSize, state);
18713
+ }
18714
+ const cursor = state.cursor;
18715
+ if (cursor === null) {
18716
+ throw new AutotaskError(`No pagination cursor available for page ${page} of ${operation}`, {
18717
+ code: "INVALID_RESPONSE",
18718
+ category: "server",
18719
+ operation
18720
+ });
18721
+ }
18722
+ const body = buildQueryBody({ ...params, maxRecords: pageSize });
18723
+ const res = await this.http.request({
18724
+ method: "POST",
18725
+ path: cursorToPath(this.http.baseUrl, cursor),
18726
+ body,
18727
+ operation,
18728
+ idempotent: true
18729
+ });
18730
+ return this.readPage(res, page, pageSize, state);
18731
+ }
18732
+ /** @internal */
18733
+ readPage(body, page, pageSize, state) {
18734
+ const { items, pageDetails } = unwrapItems(body, `${this.config.key}.query`);
18735
+ state.cursor = pageDetails.nextPageUrl ?? null;
18736
+ const hasMore = state.cursor !== null;
18737
+ return {
18738
+ items,
18739
+ page,
18740
+ pageSize,
18741
+ // Verbatim vendor echo of MaxRecords — NOT a match count (live-verified).
18742
+ requestCount: pageDetails.requestCount,
18743
+ // No vendor total exists: the pages known so far (exact on the last page).
18744
+ totalPages: hasMore ? page + 1 : items.length === 0 ? page - 1 : page,
18745
+ hasMore
18746
+ };
18747
+ }
18748
+ /** Build the per-walk fetcher (closure keeps cursor state private to this walk). */
18749
+ pageFetcher(params) {
18750
+ const state = { cursor: null };
18751
+ const p = params ?? {};
18752
+ return (page, pageSize) => this.fetchPage(p, page, pageSize, state);
18753
+ }
18754
+ /** @internal */
18755
+ walkPageSize(params) {
18756
+ return clampPageSize(params?.maxRecords ?? this.defaultPageSize);
18757
+ }
18758
+ /** Issue #281: relation filters (`ticket.<field>` …) resolve before a walk,
18759
+ * through the same rewrite as queryBounded / count. `undefined` = no parent
18760
+ * matched, so nothing can match (the walk is skipped: zero child wire). */
18761
+ async relationWalkParams(params, op) {
18762
+ const p = params ?? {};
18763
+ const relation = await rewriteRelationFilters(this.http, this.config.key, normalizedConditions(p.filter), op);
18764
+ if (relation?.never === true) return void 0;
18765
+ return relation === void 0 ? p : { ...p, filter: relation.conditions };
18766
+ }
18767
+ /**
18768
+ * Stream items across pages (the resource `query` surface — the MCP
18769
+ * server's `output_path` walk). Issue #281: relation filters resolve first
18770
+ * (as queryBounded), and each page's rows carry the default label siblings
18771
+ * (issue #276, the same `labelRows`) unless `names: false`. Labelling runs
18772
+ * per page, so the cold name-read cap (20) applies per page; a capped id
18773
+ * gets a null `*Name` (an iterator has no result envelope for the `names`
18774
+ * summary). A resource built without a client (bare `HttpClient`) yields
18775
+ * raw rows.
18776
+ */
18777
+ async *queryItems(params) {
18778
+ const { names, ...rest } = params ?? {};
18779
+ const label = readNamesOpt(names, `${this.config.key}.query`);
18780
+ const walk = await this.relationWalkParams(rest, `${this.config.key}.query`);
18781
+ if (walk === void 0) return;
18782
+ const client = this.http.client;
18783
+ if (!label || client === void 0) {
18784
+ yield* paginateItems(this.pageFetcher(walk), { pageSize: this.walkPageSize(walk) });
18785
+ return;
18786
+ }
18787
+ for await (const page of paginate(this.pageFetcher(walk), { pageSize: this.walkPageSize(walk) })) {
18788
+ const labelled = await labelRows(client, this.config.key, page.items);
18789
+ yield* labelled.rows;
18790
+ }
18791
+ }
18792
+ /** Stream page objects (the resource `queryPages` surface; relation filters resolve first, #281). */
18793
+ async *queryPageIter(params) {
18794
+ const walk = await this.relationWalkParams(params, `${this.config.key}.query`);
18795
+ if (walk === void 0) return;
18796
+ yield* paginate(this.pageFetcher(walk), { pageSize: this.walkPageSize(walk) });
18797
+ }
18798
+ /** Collect every item across all pages (the resource `queryAll` surface; 1000-page guarded; relation filters resolve first, #281). */
18799
+ async queryAllItems(params) {
18800
+ const walk = await this.relationWalkParams(params, `${this.config.key}.query`);
18801
+ if (walk === void 0) return [];
18802
+ return collectAll(this.pageFetcher(walk), { pageSize: this.walkPageSize(walk), maxRecords: walk.maxTotalRecords });
18803
+ }
18804
+ /**
18805
+ * `queryBounded` — server-side pagination (the MCP query surface): ONE call
18806
+ * walks the vendor cursor internally until `limit` TOTAL records are
18807
+ * collected or the cursor is exhausted, and returns a bounded,
18808
+ * complete-enough result. The client never owns the walk:
18809
+ * - `limit` = total records (default 500, hard max 2500 — above it throws
18810
+ * AutotaskConfigError; it is NOT a page size). The page-1 request uses
18811
+ * the internal page size min(500, limit) (the `latest` mechanism sizes
18812
+ * its walk from the matching count); every follow-up page POSTs the
18813
+ * SAME QueryModel body (`buildQueryBody`: MaxRecords, IncludeFields and
18814
+ * the preserved filter) to the absolute cursor (issue #118 — the vendor
18815
+ * honours IncludeFields there and ignores MaxRecords, serving its own
18816
+ * page size), so the walk reuses `fetchPage` (the shared cursor machinery)
18817
+ * with a per-walk closure state (walks never share state).
18818
+ * - The 1000-page guard still applies as a SOFT stop: a walk that would
18819
+ * fetch a 1001st page stops with `hasMore: true` (never a throw — this
18820
+ * is a bounded read, not the `paginate` generator's error path).
18821
+ * - `hasMore` = (truncated by limit OR guard) AND (a non-null next cursor
18822
+ * was seen at the stop point OR the final page was sliced to meet the
18823
+ * limit). Cursor exhausted, or a short/empty page with no truncation →
18824
+ * false (never claims more than the cursor + the slice say exist).
18825
+ * - `count` is always omitted: `pageDetails.count` is the current page's
18826
+ * record count (vendor-documented), never a total — `<resource>.count`
18827
+ * is the total.
18828
+ * - Issue #59: `latest: N` (validated pre-request, zero wire) delegates to
18829
+ * `latestWalk` — the count-anchored N-highest-id mechanism; ABSENT, the
18830
+ * walk below is bit-for-bit the existing path.
18831
+ * Pure read; no mutation paths involved.
18832
+ */
18833
+ async queryBounded(params) {
18834
+ if (Array.isArray(params)) {
18835
+ throw new AutotaskConfigError(
18836
+ "queryBounded: params must be an object { filter?, limit?, latest?, includeFields? } \u2014 a bare conditions array is not accepted (wrap it: { filter: [...] })"
18837
+ );
18838
+ }
18839
+ const limit = params?.limit ?? BOUNDED_QUERY_DEFAULT_LIMIT;
18840
+ if (!Number.isInteger(limit) || limit < 1 || limit > BOUNDED_QUERY_MAX_LIMIT) {
18841
+ throw new AutotaskConfigError(
18842
+ `queryBounded: limit must be an integer 1..${BOUNDED_QUERY_MAX_LIMIT} (got ${String(limit)}) \u2014 it is the TOTAL record cap, not a page size`
18843
+ );
18844
+ }
18845
+ const noteChars = params?.noteChars;
18846
+ if (noteChars !== void 0 && (!Number.isInteger(noteChars) || noteChars < 1)) {
18847
+ throw new AutotaskConfigError(
18848
+ `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)`
18849
+ );
18850
+ }
18851
+ let walkParams = params?.fieldsPreset !== void 0 ? { ...params, includeFields: resolveFieldsPreset(params.fieldsPreset, this.config.key, params.includeFields) } : params ?? {};
18852
+ const latest = walkParams.latest;
18853
+ if (latest !== void 0) {
18854
+ if (!Number.isInteger(latest) || latest < 1 || latest > BOUNDED_QUERY_MAX_LIMIT) {
18855
+ throw new AutotaskConfigError(
18856
+ `queryBounded: latest must be an integer 1..${BOUNDED_QUERY_MAX_LIMIT} (got ${String(latest)}) \u2014 it is the count of the highest-id rows to return`
18857
+ );
18858
+ }
18859
+ if (params?.limit !== void 0 && limit < latest) {
18860
+ throw new AutotaskConfigError(
18861
+ `queryBounded: limit (${String(limit)}) must be >= latest (${String(latest)}) \u2014 latest governs the returned count and limit is the walk window`
18862
+ );
18863
+ }
18864
+ }
18865
+ const relation = await rewriteRelationFilters(this.http, this.config.key, normalizedConditions(walkParams.filter), `${this.config.key}.query`);
18866
+ if (relation?.never === true) return { items: [], fetched: 0, hasMore: false, pagesWalked: 0, relationNote: relation.note };
18867
+ if (relation !== void 0) walkParams = { ...walkParams, filter: relation.conditions };
18868
+ if (latest !== void 0) {
18869
+ return this.applyNoteChars(await this.latestWalk(walkParams, Math.max(limit, latest), latest), noteChars);
18870
+ }
18871
+ const { items, pages, guard, sliced, cursor } = await this.walkBounded(walkParams, limit);
18872
+ const hasMore = (items.length >= limit || guard) && (sliced > 0 || cursor !== null);
18873
+ const result = { items, fetched: items.length, hasMore, pagesWalked: pages };
18874
+ this.stampEmptyQueryResult(result, params);
18875
+ return this.applyNoteChars(result, noteChars);
18876
+ }
18877
+ /** Issue #61 (slice A): the noteChars truncation at the SINGLE chokepoint
18878
+ * of `queryBounded` — the normal walk and the `latest` paths both
18879
+ * produced their `BoundedQueryResult` here (the single chokepoint the
18880
+ * plan pins: R5). Copy-on-truncate (the input records are never
18881
+ * mutated); the additive `noteCharsTruncated` stamp = the truncated-
18882
+ * field count, ABSENT when 0. The resource's untrusted-class field set
18883
+ * is the generated stamp (the SAME plan column the registry's
18884
+ * `metadata.untrustedFields` channel stamps — zero second source of
18885
+ * truth); an absent set = the cap is a no-op for the resource (bit-for-
18886
+ * bit the pre-slice result).
18887
+ * @internal */
18888
+ applyNoteChars(result, noteChars) {
18889
+ if (noteChars === void 0) return result;
18890
+ const fields = UNTRUSTED_FIELDS_BY_RESOURCE[this.config.key];
18891
+ if (fields === void 0 || fields.length === 0) return result;
18892
+ const [items, truncated] = truncateNoteChars(result.items, noteChars, fields);
18893
+ if (truncated > 0) {
18894
+ return { ...result, items, noteCharsTruncated: truncated };
18895
+ }
18896
+ return result;
18897
+ }
18898
+ /** The bounded walk loop (shared by the normal `queryBounded` path and the
18899
+ * `latest` modes): walks the vendor cursor until `limit` TOTAL records
18900
+ * are collected or the cursor is exhausted; the 1000-page guard is a
18901
+ * SOFT stop (never a throw). ONE per-walk closure (walks never share
18902
+ * state).
18903
+ * @internal */
18904
+ async walkBounded(p, limit) {
18905
+ const state = { cursor: null };
18906
+ const items = [];
18907
+ let pages = 0;
18908
+ let sliced = 0;
18909
+ let guard = false;
18910
+ const pageSize = Math.min(MAX_PAGE_SIZE, limit);
18911
+ for (; ; ) {
18912
+ if (pages >= MAX_PAGES) {
18913
+ guard = true;
18914
+ break;
18915
+ }
18916
+ const page = await this.fetchPage(p, pages + 1, pageSize, state);
18917
+ pages += 1;
18918
+ const remaining = limit - items.length;
18919
+ const take = page.items.slice(0, remaining);
18920
+ sliced += Math.max(0, page.items.length - remaining);
18921
+ items.push(...take);
18922
+ if (items.length >= limit) break;
18923
+ if (!page.hasMore || page.items.length === 0) break;
18924
+ }
18925
+ return { items, pages, guard, sliced, cursor: state.cursor };
18926
+ }
18927
+ /** A walk stop is TRUNCATED when the 1000-page guard fired, or the limit
18928
+ * was met with a live next cursor (or a sliced final page): rows exist
18929
+ * past the stop point, and a top-N-by-id over the collected set alone is
18930
+ * unprovable (the `latest` mechanism refuses instead of guessing).
18931
+ * @internal */
18932
+ walkTruncated(limit, stop) {
18933
+ return stop.guard || stop.itemsLength >= limit && (stop.sliced > 0 || stop.cursor !== null);
18934
+ }
18935
+ /** The 0-item result stamping — the ONE source of truth (the normal walk
18936
+ * and the `latest` empty outcome both ride it): the typed blind-surface
18937
+ * sentinel + the pinned empty-reason, through the filter-aware predicate
18055
18938
  * `blindStampFor` (`unfiltered-blind` surfaces stamp ONLY the unfiltered
18056
18939
  * call — a filtered empty result is a true absence; live-verified
18057
18940
  * S12-B P1: the filtered tickets.query returns data on ww29). Additive:
@@ -18335,7 +19218,9 @@ var QueryBase = class {
18335
19218
  * or, on a plan-pinned blind surface, the `{ count: 0, blindSurface }`
18336
19219
  * typed sentinel (S12-C T10; see `BlindCountResult`). */
18337
19220
  async countGlobal(params) {
18338
- const conditions = normalizedConditions(params?.filter);
19221
+ const relation = await rewriteRelationFilters(this.http, this.config.key, normalizedConditions(params?.filter), `${this.config.key}.count`);
19222
+ if (relation?.never === true) return 0;
19223
+ const conditions = relation !== void 0 ? relation.conditions : normalizedConditions(params?.filter);
18339
19224
  const body = {};
18340
19225
  body.filter = conditions === void 0 ? [] : buildFilter(conditions).filter;
18341
19226
  const res = await this.http.request({
@@ -19385,1453 +20270,1075 @@ var MutationBase = class extends MetaBase {
19385
20270
  * `lastModifiedDate` and the model carries the field → fetch the current
19386
20271
  * record and compare before the mutating request (mismatch throws
19387
20272
  * STALE_OBJECT; a vanished record propagates the vendor's NotFoundError).
19388
- */
19389
- async guardStaleBeforeMutation(id, opts, operation, parentId) {
19390
- if (opts.lastModifiedDate === void 0 || this.config.staleField === void 0) return;
19391
- let current;
19392
- try {
19393
- current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
19394
- } catch (err) {
19395
- if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
19396
- throw err;
19397
- }
19398
- this.staleObjectCheck(current, opts, operation);
19399
- }
19400
- /**
19401
- * `create` dry-run: input validation + parent verification (scoped
19402
- * resources) + the pinned envelope. NO request is issued (scoped parent
19403
- * verification is a read). Duplicate detection has no reliable lookup for
19404
- * a plain create (that is `createIfNotExists`), so it is not claimed.
19405
- */
19406
- async runCreateDryRun(data, parentId) {
19407
- const checks = [];
19408
- const warnings = [];
19409
- checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
19410
- const deleteBoundaryNote = DELETE_BOUNDARY_NOTES[this.config.key];
19411
- if (deleteBoundaryNote !== void 0) {
19412
- checks.push({ name: "delete-boundary", ok: true });
19413
- warnings.push(deleteBoundaryNote);
19414
- }
19415
- let path = this.resourcePath;
19416
- if (parentId !== void 0) {
19417
- path = this.scopedCollectionPath(parentId);
19418
- const parentPath = this.config.parentPath;
19419
- if (parentPath === void 0) {
19420
- throw new AutotaskConfigError(
19421
- `dry-run: ${this.config.key} is parent-scoped but has no parentPath configured`
19422
- );
19423
- }
19424
- let parent = null;
19425
- if (typeof parentId === "number" && Number.isInteger(parentId) && parentId >= 0) {
19426
- try {
19427
- parent = await this.fetchRecordRaw(parentPath, parentId, true);
19428
- } catch (err) {
19429
- if (!(err instanceof NotFoundError)) throw err;
19430
- }
19431
- }
19432
- checks.push({ name: "parent-exists", ok: parent !== null });
19433
- if (parent === null) {
19434
- warnings.push(`parent ${parentId} not found; the vendor would reject the create`);
19435
- }
19436
- }
19437
- if (isRecord2(data)) {
19438
- const eff = effectiveRequiredFields(void 0, RELATIONSHIP_MAP[this.config.key]?.zoneFacts ?? null);
19439
- const missing = eff.fields.filter((f) => f !== this.config.parentField && data[f] === void 0);
19440
- if (missing.length > 0) {
19441
- warnings.push(`required field(s) not in the data: ${missing.join(", ")} (${eff.from}); the vendor would likely reject the create`);
19442
- }
19443
- }
19444
- if (this.config.key === "companies") {
19445
- 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)');
19446
- }
19447
- return buildDryRunResult({
19448
- operation: `${this.config.key}.create`,
19449
- method: "POST",
19450
- path,
19451
- ids: [],
19452
- checks,
19453
- impact: { affected: 1, scope: "single-record", reversible: true },
19454
- warnings,
19455
- data
19456
- }, { redactPersonalData: this.http.redactPersonalData });
19457
- }
19458
- /**
19459
- * `update` dry-run: input validation + current-record fetch (a missing
19460
- * record carries `warnings` and no `diff` — pinned), the stale-object
19461
- * check (mismatch throws STALE_OBJECT) and the best-effort `diff`. NO
19462
- * mutating request is issued.
19463
- */
19464
- async runUpdateDryRun(id, data, opts = {}, parentId) {
19465
- const operation = `${this.config.key}.update`;
19466
- const checks = [];
19467
- const warnings = [];
19468
- checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
19469
- const scopedUpdate = parentId !== void 0 && this.config.parentScopedPath !== void 0;
19470
- const collectionUpdate = scopedUpdate || this.config.collectionUpdate === true;
19471
- const path = scopedUpdate ? this.scopedCollectionPath(parentId) : collectionUpdate ? this.resourcePath : `${this.resourcePath}/${id}`;
19472
- const method = scopedUpdate ? "PATCH" : "PUT";
19473
- let current = null;
19474
- try {
19475
- current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
19476
- } catch (err) {
19477
- if (err instanceof NotFoundError) {
19478
- const updateReason = EMPTY_REASON_STAMPS[this.config.key]?.update;
19479
- checks.push({
19480
- name: "current-record-exists",
19481
- ok: false,
19482
- ...updateReason !== void 0 ? { note: updateReason.note } : {}
19483
- });
19484
- warnings.push(`current record ${id} not fetchable; no diff`);
19485
- return buildDryRunResult({
19486
- operation,
19487
- method,
19488
- path,
19489
- ids: [id],
19490
- checks,
19491
- impact: { affected: 1, scope: "single-record", reversible: true },
19492
- warnings,
19493
- data,
19494
- ...updateReason !== void 0 ? { emptyReason: updateReason } : {}
19495
- }, { redactPersonalData: this.http.redactPersonalData });
19496
- }
19497
- throw err;
19498
- }
19499
- checks.push({ name: "current-record-exists", ok: true });
19500
- if (this.staleObjectCheck(current, opts, operation)) {
19501
- checks.push({ name: "stale-check", ok: true });
19502
- }
19503
- let envelopeData = data;
19504
- if (parentId === void 0 && this.config.collectionUpdate === true && opts.raw !== true) {
19505
- envelopeData = mergePartialBody(
19506
- current,
19507
- this.normalizeMutationBody(data)
19508
- );
19509
- }
19510
- return buildDryRunResult({
19511
- operation,
19512
- method,
19513
- path,
19514
- ids: [id],
19515
- checks,
19516
- diff: computeFieldDiff(current, data),
19517
- impact: { affected: 1, scope: "single-record", reversible: true },
19518
- warnings,
19519
- data: envelopeData
19520
- }, { redactPersonalData: this.http.redactPersonalData });
19521
- }
19522
- /**
19523
- * `delete` dry-run: current-record verification + the stale-object check +
19524
- * the pinned envelope (no diff — a delete has no before/after fields). NO
19525
- * mutating request is issued.
19526
- */
19527
- async runDeleteDryRun(id, opts = {}, parentId) {
19528
- const operation = `${this.config.key}.delete`;
19529
- const checks = [];
19530
- const warnings = [];
19531
- const path = parentId === void 0 ? `${this.resourcePath}/${id}` : this.scopedItemPath(parentId, id);
19532
- let current = null;
20273
+ */
20274
+ async guardStaleBeforeMutation(id, opts, operation, parentId) {
20275
+ if (opts.lastModifiedDate === void 0 || this.config.staleField === void 0) return;
20276
+ let current;
19533
20277
  try {
19534
20278
  current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
19535
20279
  } catch (err) {
19536
- if (err instanceof NotFoundError) {
19537
- checks.push({ name: "current-record-exists", ok: false });
19538
- warnings.push(`record ${id} not fetchable; the vendor would reject the delete`);
19539
- return buildDryRunResult({
19540
- operation,
19541
- method: "DELETE",
19542
- path,
19543
- ids: [id],
19544
- checks,
19545
- impact: { affected: 1, scope: "single-record", reversible: false },
19546
- warnings
19547
- }, { redactPersonalData: this.http.redactPersonalData });
19548
- }
20280
+ if (err instanceof NotFoundError) attachUpdateTargetMissReason(err, this.config.key);
19549
20281
  throw err;
19550
20282
  }
19551
- checks.push({ name: "current-record-exists", ok: true });
19552
- if (this.staleObjectCheck(current, opts, operation)) {
19553
- checks.push({ name: "stale-check", ok: true });
19554
- }
19555
- return buildDryRunResult({
19556
- operation,
19557
- method: "DELETE",
19558
- path,
19559
- ids: [id],
19560
- checks,
19561
- impact: { affected: 1, scope: "single-record", reversible: false },
19562
- warnings
19563
- }, { redactPersonalData: this.http.redactPersonalData });
19564
- }
19565
- };
19566
-
19567
- // src/resources/base/BaseResource.ts
19568
- var BaseResource = class extends MutationBase {
19569
- };
19570
-
19571
- // src/resources/billing_codes.ts
19572
- var BillingCodesResource = class extends BaseResource {
19573
- constructor(http, pageSize) {
19574
- super(http, {
19575
- key: "billing_codes",
19576
- resourcePath: "/V1.0/BillingCodes",
19577
- deletable: false,
19578
- udfCapable: false,
19579
- searchFields: ["name"]
19580
- }, pageSize);
19581
- }
19582
- /** Query BillingCode records with the G3 filter surface (server-side JSON filter). */
19583
- query(params) {
19584
- return this.queryItems(params);
19585
- }
19586
- /** Get every BillingCode record (the MCP-preferred read; 1000-page guarded). */
19587
- async queryAll(params) {
19588
- return this.queryAllItems(params);
19589
- }
19590
- /** Count matching records (queryCount envelope unwrapped to a plain number). */
19591
- async count(params) {
19592
- return this.countGlobal(params);
19593
- }
19594
- /** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
19595
- async get(id, opts = {}) {
19596
- return this.getGlobal(id, opts?.fresh === true);
19597
- }
19598
- /** Resolve a BillingCode from a numeric id (or, where the vendor indexes a primary text field,
19599
- * from an exact match on name). A numeric id miss throws NOT_FOUND; a complete
19600
- * non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
19601
- * cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
19602
- * The compact shape is the structural alias of the full record; `expand: true` is the
19603
- * type-level escape hatch. */
19604
- async resolve(identifier, opts = {}) {
19605
- void opts;
19606
- return this.resolveBase(identifier);
19607
- }
19608
- /** Search BillingCode records: bounded contains on name (no vendor free-text search exists);
19609
- * limit defaults to 25, max 500. */
19610
- async search(query, opts) {
19611
- return this.searchBase(query, { limit: opts?.limit });
19612
- }
19613
- };
19614
-
19615
- // src/resources/special/dateWindow.ts
19616
- var DAY_MS = 864e5;
19617
- var DEFAULT_LIMIT = 500;
19618
- var MAX_LIMIT = 500;
19619
- function clampWindowLimit(limit) {
19620
- const n = limit === void 0 ? DEFAULT_LIMIT : Math.trunc(limit);
19621
- if (!Number.isInteger(n) || n < 1) {
19622
- throw new AutotaskConfigError(`limit must be an integer >= 1 (got ${String(limit)})`);
19623
- }
19624
- return Math.min(n, MAX_LIMIT);
19625
- }
19626
- var iso = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
19627
- var WEEKDAYS = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
19628
- function wallParts(tz, ms) {
19629
- const parts = new Intl.DateTimeFormat("en-US", {
19630
- timeZone: tz,
19631
- year: "numeric",
19632
- month: "2-digit",
19633
- day: "2-digit",
19634
- hour: "2-digit",
19635
- minute: "2-digit",
19636
- second: "2-digit",
19637
- hour12: false,
19638
- weekday: "short"
19639
- }).formatToParts(new Date(ms));
19640
- const w = {};
19641
- let weekday = -1;
19642
- for (const p of parts) {
19643
- switch (p.type) {
19644
- case "year":
19645
- w.y = Number(p.value);
19646
- break;
19647
- case "month":
19648
- w.m = Number(p.value);
19649
- break;
19650
- case "day":
19651
- w.d = Number(p.value);
19652
- break;
19653
- case "hour":
19654
- w.h = Number(p.value) % 24;
19655
- break;
19656
- case "minute":
19657
- w.mi = Number(p.value);
19658
- break;
19659
- case "second":
19660
- w.s = Number(p.value);
19661
- break;
19662
- case "weekday":
19663
- weekday = WEEKDAYS[p.value] ?? -1;
19664
- break;
19665
- }
19666
- }
19667
- if (weekday < 0 || [w.y, w.m, w.d, w.h, w.mi, w.s].some((v) => v === void 0 || Number.isNaN(v))) {
19668
- throw new AutotaskConfigError(`dateWindow: could not read the wall-clock parts for timezone "${tz}" (the Intl format returned an incomplete or unmapped part)`);
19669
- }
19670
- return { y: w.y, m: w.m, d: w.d, h: w.h, mi: w.mi, s: w.s, weekday };
19671
- }
19672
- function tzOffsetMs(tz, ms) {
19673
- const p = wallParts(tz, ms);
19674
- return Date.UTC(p.y, p.m - 1, p.d, p.h, p.mi, p.s) - ms;
19675
- }
19676
- function localDayStartMs(y, m, d, tz) {
19677
- if (tz === void 0) return Date.UTC(y, m - 1, d);
19678
- const candidate2 = Date.UTC(y, m - 1, d);
19679
- const offset = tzOffsetMs(tz, candidate2);
19680
- let start = candidate2 - offset;
19681
- if (tzOffsetMs(tz, start) !== offset) {
19682
- start = candidate2 - tzOffsetMs(tz, start);
19683
- }
19684
- const p = wallParts(tz, start);
19685
- if (p.d !== d) start += 864e5 - ((p.h * 60 + p.mi) * 60 + p.s) * 1e3;
19686
- return start;
19687
- }
19688
- function localDayEndMs(y, m, d, tz) {
19689
- if (tz === void 0) return Date.UTC(y, m - 1, d) + DAY_MS - 1;
19690
- const next = new Date(Date.UTC(y, m - 1, d + 1));
19691
- return localDayStartMs(next.getUTCFullYear(), next.getUTCMonth() + 1, next.getUTCDate(), tz) - 1;
19692
- }
19693
- function validateZone(tz, opName = "dateWindow") {
19694
- if (typeof tz !== "string" || tz.length === 0) {
19695
- throw new AutotaskConfigError(
19696
- `${opName}: tz must be a non-empty IANA timezone string (got ${JSON.stringify(tz)}; e.g. 'Australia/Sydney' \u2014 omitted tz = UTC)`,
19697
- { operation: opName }
19698
- );
19699
- }
19700
- try {
19701
- new Intl.DateTimeFormat("en-US", { timeZone: tz });
19702
- } catch {
19703
- throw new AutotaskConfigError(
19704
- `${opName}: tz is not a valid IANA timezone (${JSON.stringify(tz)}; e.g. 'Australia/Sydney' \u2014 omitted tz = UTC)`,
19705
- { operation: opName }
19706
- );
20283
+ this.staleObjectCheck(current, opts, operation);
19707
20284
  }
19708
- return tz;
19709
- }
19710
- var utcDayStart = (ms) => {
19711
- const d = new Date(ms);
19712
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
19713
- };
19714
- var utcDayEnd = (ms) => utcDayStart(ms) + DAY_MS - 1;
19715
- function utcDateWindowBounds(kind, now) {
19716
- const t = now.getTime();
19717
- let from;
19718
- let to;
19719
- switch (kind) {
19720
- case "today":
19721
- from = utcDayStart(t);
19722
- to = utcDayEnd(t);
19723
- break;
19724
- case "yesterday":
19725
- from = utcDayStart(t - DAY_MS);
19726
- to = utcDayEnd(t - DAY_MS);
19727
- break;
19728
- case "last24h":
19729
- from = t - DAY_MS;
19730
- to = t;
19731
- break;
19732
- case "last7":
19733
- case "last14":
19734
- case "last30":
19735
- case "last90": {
19736
- const days = Number(kind.slice(4));
19737
- from = utcDayStart(t - days * DAY_MS);
19738
- to = utcDayEnd(t);
19739
- break;
19740
- }
19741
- case "lastFullWeek":
19742
- case "lastFullFortnight": {
19743
- const d = new Date(t);
19744
- const daysSinceMonday = (d.getUTCDay() + 6) % 7;
19745
- const thisMonday = utcDayStart(t) - daysSinceMonday * DAY_MS;
19746
- const daysBack = kind === "lastFullWeek" ? 7 : 14;
19747
- from = thisMonday - daysBack * DAY_MS;
19748
- to = thisMonday - 1;
19749
- break;
20285
+ /**
20286
+ * `create` dry-run: input validation + parent verification (scoped
20287
+ * resources) + the pinned envelope. NO request is issued (scoped parent
20288
+ * verification is a read). Duplicate detection has no reliable lookup for
20289
+ * a plain create (that is `createIfNotExists`), so it is not claimed.
20290
+ */
20291
+ async runCreateDryRun(data, parentId) {
20292
+ const checks = [];
20293
+ const warnings = [];
20294
+ checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
20295
+ const deleteBoundaryNote = DELETE_BOUNDARY_NOTES[this.config.key];
20296
+ if (deleteBoundaryNote !== void 0) {
20297
+ checks.push({ name: "delete-boundary", ok: true });
20298
+ warnings.push(deleteBoundaryNote);
19750
20299
  }
19751
- case "lastFullMonth": {
19752
- const d = new Date(t);
19753
- const y = d.getUTCFullYear();
19754
- const m = d.getUTCMonth() - 1;
19755
- from = Date.UTC(y, m, 1);
19756
- to = Date.UTC(y, m + 1, 1) - 1;
19757
- break;
20300
+ let path = this.resourcePath;
20301
+ if (parentId !== void 0) {
20302
+ path = this.scopedCollectionPath(parentId);
20303
+ const parentPath = this.config.parentPath;
20304
+ if (parentPath === void 0) {
20305
+ throw new AutotaskConfigError(
20306
+ `dry-run: ${this.config.key} is parent-scoped but has no parentPath configured`
20307
+ );
20308
+ }
20309
+ let parent = null;
20310
+ if (typeof parentId === "number" && Number.isInteger(parentId) && parentId >= 0) {
20311
+ try {
20312
+ parent = await this.fetchRecordRaw(parentPath, parentId, true);
20313
+ } catch (err) {
20314
+ if (!(err instanceof NotFoundError)) throw err;
20315
+ }
20316
+ }
20317
+ checks.push({ name: "parent-exists", ok: parent !== null });
20318
+ if (parent === null) {
20319
+ warnings.push(`parent ${parentId} not found; the vendor would reject the create`);
20320
+ }
19758
20321
  }
19759
- case "lastFullQuarter": {
19760
- const d = new Date(t);
19761
- let q = Math.floor(d.getUTCMonth() / 3) - 1;
19762
- let y = d.getUTCFullYear();
19763
- if (q < 0) {
19764
- q = 3;
19765
- y -= 1;
20322
+ if (isRecord2(data)) {
20323
+ const eff = effectiveRequiredFields(void 0, RELATIONSHIP_MAP[this.config.key]?.zoneFacts ?? null);
20324
+ const missing = eff.fields.filter((f) => f !== this.config.parentField && data[f] === void 0);
20325
+ if (missing.length > 0) {
20326
+ warnings.push(`required field(s) not in the data: ${missing.join(", ")} (${eff.from}); the vendor would likely reject the create`);
19766
20327
  }
19767
- const first = q * 3;
19768
- from = Date.UTC(y, first, 1);
19769
- to = Date.UTC(y, first + 3, 1) - 1;
19770
- break;
19771
20328
  }
20329
+ if (this.config.key === "companies") {
20330
+ 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)');
20331
+ }
20332
+ return buildDryRunResult({
20333
+ operation: `${this.config.key}.create`,
20334
+ method: "POST",
20335
+ path,
20336
+ ids: [],
20337
+ checks,
20338
+ impact: { affected: 1, scope: "single-record", reversible: true },
20339
+ warnings,
20340
+ data
20341
+ }, { redactPersonalData: this.http.redactPersonalData });
19772
20342
  }
19773
- return { from: iso(from), to: iso(to) };
19774
- }
19775
- function dateWindowBounds(kind, now, tz) {
19776
- if (tz === void 0) return utcDateWindowBounds(kind, now);
19777
- const zone = validateZone(tz);
19778
- const t = now.getTime();
19779
- if (kind === "last24h") {
19780
- return { from: iso(t - DAY_MS), to: iso(t) };
19781
- }
19782
- const { y, m, d, weekday } = wallParts(zone, t);
19783
- const shifted = (dy) => new Date(Date.UTC(y, m - 1, d + dy));
19784
- const dayStart = (dy) => {
19785
- const dt = shifted(dy);
19786
- return localDayStartMs(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate(), zone);
19787
- };
19788
- const dayStartAt = (instant) => {
19789
- const p = wallParts(zone, instant);
19790
- return localDayStartMs(p.y, p.m, p.d, zone);
19791
- };
19792
- const dayEndAt = (instant) => {
19793
- const p = wallParts(zone, instant);
19794
- return localDayEndMs(p.y, p.m, p.d, zone);
19795
- };
19796
- let from;
19797
- let to;
19798
- switch (kind) {
19799
- case "today":
19800
- from = dayStartAt(t);
19801
- to = dayEndAt(t);
19802
- break;
19803
- case "yesterday":
19804
- from = dayStart(-1);
19805
- to = dayStart(0) - 1;
19806
- break;
19807
- case "last7":
19808
- case "last14":
19809
- case "last30":
19810
- case "last90": {
19811
- const days = Number(kind.slice(4));
19812
- from = dayStart(-days);
19813
- to = dayEndAt(t);
19814
- break;
20343
+ /**
20344
+ * `update` dry-run: input validation + current-record fetch (a missing
20345
+ * record carries `warnings` and no `diff` — pinned), the stale-object
20346
+ * check (mismatch throws STALE_OBJECT) and the best-effort `diff`. NO
20347
+ * mutating request is issued.
20348
+ */
20349
+ async runUpdateDryRun(id, data, opts = {}, parentId) {
20350
+ const operation = `${this.config.key}.update`;
20351
+ const checks = [];
20352
+ const warnings = [];
20353
+ checks.push({ name: "input-validation", ok: isRecord2(data) && Object.keys(data).length > 0 });
20354
+ const scopedUpdate = parentId !== void 0 && this.config.parentScopedPath !== void 0;
20355
+ const collectionUpdate = scopedUpdate || this.config.collectionUpdate === true;
20356
+ const path = scopedUpdate ? this.scopedCollectionPath(parentId) : collectionUpdate ? this.resourcePath : `${this.resourcePath}/${id}`;
20357
+ const method = scopedUpdate ? "PATCH" : "PUT";
20358
+ let current = null;
20359
+ try {
20360
+ current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
20361
+ } catch (err) {
20362
+ if (err instanceof NotFoundError) {
20363
+ const updateReason = EMPTY_REASON_STAMPS[this.config.key]?.update;
20364
+ checks.push({
20365
+ name: "current-record-exists",
20366
+ ok: false,
20367
+ ...updateReason !== void 0 ? { note: updateReason.note } : {}
20368
+ });
20369
+ warnings.push(`current record ${id} not fetchable; no diff`);
20370
+ return buildDryRunResult({
20371
+ operation,
20372
+ method,
20373
+ path,
20374
+ ids: [id],
20375
+ checks,
20376
+ impact: { affected: 1, scope: "single-record", reversible: true },
20377
+ warnings,
20378
+ data,
20379
+ ...updateReason !== void 0 ? { emptyReason: updateReason } : {}
20380
+ }, { redactPersonalData: this.http.redactPersonalData });
20381
+ }
20382
+ throw err;
19815
20383
  }
19816
- case "lastFullWeek":
19817
- case "lastFullFortnight": {
19818
- const daysSinceMonday = (weekday + 6) % 7;
19819
- const daysBack = kind === "lastFullWeek" ? 7 : 14;
19820
- from = dayStart(-(daysSinceMonday + daysBack));
19821
- to = dayStart(-daysSinceMonday) - 1;
19822
- break;
20384
+ checks.push({ name: "current-record-exists", ok: true });
20385
+ if (this.staleObjectCheck(current, opts, operation)) {
20386
+ checks.push({ name: "stale-check", ok: true });
19823
20387
  }
19824
- case "lastFullMonth": {
19825
- const prev = new Date(Date.UTC(y, m - 2, 1));
19826
- from = localDayStartMs(prev.getUTCFullYear(), prev.getUTCMonth() + 1, 1, zone);
19827
- to = localDayStartMs(y, m, 1, zone) - 1;
19828
- break;
20388
+ let envelopeData = data;
20389
+ if (parentId === void 0 && this.config.collectionUpdate === true && opts.raw !== true) {
20390
+ envelopeData = mergePartialBody(
20391
+ current,
20392
+ this.normalizeMutationBody(data)
20393
+ );
19829
20394
  }
19830
- case "lastFullQuarter": {
19831
- const q = Math.floor((m - 1) / 3);
19832
- const prevQ = q - 1 < 0 ? 3 : q - 1;
19833
- const prevY = q - 1 < 0 ? y - 1 : y;
19834
- from = localDayStartMs(prevY, prevQ * 3 + 1, 1, zone);
19835
- to = localDayStartMs(y, q * 3 + 1, 1, zone) - 1;
19836
- break;
20395
+ return buildDryRunResult({
20396
+ operation,
20397
+ method,
20398
+ path,
20399
+ ids: [id],
20400
+ checks,
20401
+ diff: computeFieldDiff(current, data),
20402
+ impact: { affected: 1, scope: "single-record", reversible: true },
20403
+ warnings,
20404
+ data: envelopeData
20405
+ }, { redactPersonalData: this.http.redactPersonalData });
20406
+ }
20407
+ /**
20408
+ * `delete` dry-run: current-record verification + the stale-object check +
20409
+ * the pinned envelope (no diff — a delete has no before/after fields). NO
20410
+ * mutating request is issued.
20411
+ */
20412
+ async runDeleteDryRun(id, opts = {}, parentId) {
20413
+ const operation = `${this.config.key}.delete`;
20414
+ const checks = [];
20415
+ const warnings = [];
20416
+ const path = parentId === void 0 ? `${this.resourcePath}/${id}` : this.scopedItemPath(parentId, id);
20417
+ let current = null;
20418
+ try {
20419
+ current = parentId === void 0 ? await this.getGlobal(id, true) : this.config.attachmentGet ? await this.getScopedModel(parentId, id, true) : await this.getScopedRaw(parentId, id, true);
20420
+ } catch (err) {
20421
+ if (err instanceof NotFoundError) {
20422
+ checks.push({ name: "current-record-exists", ok: false });
20423
+ warnings.push(`record ${id} not fetchable; the vendor would reject the delete`);
20424
+ return buildDryRunResult({
20425
+ operation,
20426
+ method: "DELETE",
20427
+ path,
20428
+ ids: [id],
20429
+ checks,
20430
+ impact: { affected: 1, scope: "single-record", reversible: false },
20431
+ warnings
20432
+ }, { redactPersonalData: this.http.redactPersonalData });
20433
+ }
20434
+ throw err;
20435
+ }
20436
+ checks.push({ name: "current-record-exists", ok: true });
20437
+ if (this.staleObjectCheck(current, opts, operation)) {
20438
+ checks.push({ name: "stale-check", ok: true });
19837
20439
  }
20440
+ return buildDryRunResult({
20441
+ operation,
20442
+ method: "DELETE",
20443
+ path,
20444
+ ids: [id],
20445
+ checks,
20446
+ impact: { affected: 1, scope: "single-record", reversible: false },
20447
+ warnings
20448
+ }, { redactPersonalData: this.http.redactPersonalData });
19838
20449
  }
19839
- return { from: iso(from), to: iso(to) };
20450
+ };
20451
+
20452
+ // src/resources/base/BaseResource.ts
20453
+ var BaseResource = class extends MutationBase {
20454
+ };
20455
+
20456
+ // src/operations/compositeNames.ts
20457
+ 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.)";
20458
+ var DEFAULT_MAX_NAME_READS = 20;
20459
+ var TICKET_LABEL_FIELDS = ["status", "priority", "ticketType", "issueType", "subIssueType", "source", "queueID"];
20460
+ function isVendorId(value) {
20461
+ if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0;
20462
+ if (typeof value === "string") return /^\d+$/.test(value);
20463
+ return false;
19840
20464
  }
19841
- function dateOnlyWindowBounds(kind, now, tz) {
19842
- if (tz === void 0 || kind === "last24h") return dateWindowBounds(kind, now, tz);
19843
- const zone = validateZone(tz);
19844
- const { from, to } = dateWindowBounds(kind, now, zone);
19845
- const f = wallParts(zone, Date.parse(from));
19846
- const t = wallParts(zone, Date.parse(to));
19847
- return { from: iso(Date.UTC(f.y, f.m - 1, f.d)), to: iso(Date.UTC(t.y, t.m - 1, t.d) + DAY_MS - 1) };
20465
+ function createNamePlan(labelFields = [], labelResource) {
20466
+ const resourceIds = [];
20467
+ const resSeen = /* @__PURE__ */ new Set();
20468
+ const recordFks = [];
20469
+ const fkSeen = /* @__PURE__ */ new Set();
20470
+ let lineOfBusinessId;
20471
+ let lineOfBusinessSource;
20472
+ let lineOfBusinessContractId;
20473
+ return {
20474
+ plan: () => ({
20475
+ resourceIds,
20476
+ recordFks,
20477
+ labelFields,
20478
+ ...labelResource !== void 0 ? { labelResource } : {},
20479
+ ...lineOfBusinessId !== void 0 ? { lineOfBusinessId } : {},
20480
+ ...lineOfBusinessSource !== void 0 ? { lineOfBusinessSource } : {},
20481
+ ...lineOfBusinessContractId !== void 0 ? { lineOfBusinessContractId } : {}
20482
+ }),
20483
+ addResource: (value) => {
20484
+ if (!isVendorId(value)) return;
20485
+ const k = String(value);
20486
+ if (!resSeen.has(k)) {
20487
+ resSeen.add(k);
20488
+ resourceIds.push(value);
20489
+ }
20490
+ },
20491
+ addFk: (table, value) => {
20492
+ if (!isVendorId(value)) return;
20493
+ const k = `${table}:${String(value)}`;
20494
+ if (!fkSeen.has(k)) {
20495
+ fkSeen.add(k);
20496
+ recordFks.push({ table, id: value });
20497
+ }
20498
+ },
20499
+ addLineOfBusinessFromContract: (value) => {
20500
+ if (!isVendorId(value)) return;
20501
+ lineOfBusinessContractId = value;
20502
+ },
20503
+ addLineOfBusiness: (value, origin) => {
20504
+ if (!isVendorId(value)) return;
20505
+ lineOfBusinessId = value;
20506
+ if (origin !== void 0) {
20507
+ lineOfBusinessSource = origin.source;
20508
+ if (origin.contractId !== void 0) lineOfBusinessContractId = origin.contractId;
20509
+ }
20510
+ }
20511
+ };
19848
20512
  }
19849
- async function dateWindowQuery(http, path, field, kind, limit, operation, now, tz) {
19850
- const zone = tz === void 0 ? void 0 : validateZone(tz, operation);
19851
- const { from, to } = dateOnlyWindowBounds(kind, now, zone);
19852
- const filter = [
19853
- { field, op: "gte", value: from },
19854
- { field, op: "lte", value: to }
19855
- ];
19856
- const res = await http.request({
19857
- method: "POST",
19858
- idempotent: true,
19859
- path,
19860
- body: { MaxRecords: limit, filter },
19861
- operation
19862
- });
19863
- return unwrapItems(res).items;
20513
+ function addNameSibling(row, field, resolve) {
20514
+ const value = row[field];
20515
+ if (value === void 0) return;
20516
+ row[`${field}Name`] = isVendorId(value) ? resolve(value) : null;
19864
20517
  }
19865
- var isoUtc = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
19866
- function customRangeDay(value, label, zone, opName) {
19867
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
19868
- if (!m) {
19869
- throw new AutotaskConfigError(
19870
- zone === void 0 ? `${opName}: ${label} must be a UTC 'YYYY-MM-DD' date (got ${JSON.stringify(value)})` : `${opName}: ${label} must be a 'YYYY-MM-DD' local calendar day in ${zone} (got ${JSON.stringify(value)})`,
19871
- { operation: opName }
19872
- );
19873
- }
19874
- const y = Number(m[1]);
19875
- const mo = Number(m[2]);
19876
- const d = Number(m[3]);
19877
- const probe = new Date(Date.UTC(y, mo - 1, d));
19878
- const real = probe.getUTCFullYear() === y && probe.getUTCMonth() === mo - 1 && probe.getUTCDate() === d;
19879
- const ms = !real ? NaN : zone === void 0 ? Date.UTC(y, mo - 1, d) : localDayStartMs(y, mo, d, zone);
19880
- if (!Number.isFinite(ms)) {
19881
- throw new AutotaskConfigError(
19882
- zone === void 0 ? `${opName}: ${label} is not a valid UTC date (${JSON.stringify(value)})` : `${opName}: ${label} is not a valid calendar day in ${zone} (${JSON.stringify(value)})`,
19883
- { operation: opName }
19884
- );
19885
- }
19886
- return { y, mo, d, ms };
20518
+ function addLabelSibling(row, field, names) {
20519
+ const value = row[field];
20520
+ if (value === void 0) return;
20521
+ row[`${field}Label`] = names.labelFor(field, value);
19887
20522
  }
19888
- async function customRangeQuery(http, path, field, from, to, limit, operation, zone) {
19889
- if (zone !== void 0) validateZone(zone, operation);
19890
- const fromB = customRangeDay(from, "from", void 0, operation);
19891
- const toB = customRangeDay(to, "to", void 0, operation);
19892
- if (fromB.ms > toB.ms) {
19893
- throw new AutotaskConfigError(
19894
- `${operation}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`,
19895
- { operation }
19896
- );
20523
+ var FK_TABLE_PATH = {
20524
+ role: "/V1.0/Roles",
20525
+ billingCode: "/V1.0/BillingCodes",
20526
+ contract: "/V1.0/Contracts",
20527
+ company: "/V1.0/Companies",
20528
+ ticket: "/V1.0/Tickets"
20529
+ };
20530
+ var FK_NAME_FIELD = {
20531
+ role: "name",
20532
+ billingCode: "name",
20533
+ contract: "contractName",
20534
+ company: "companyName",
20535
+ ticket: "ticketNumber"
20536
+ };
20537
+ function fkName(record, table) {
20538
+ if (table === "ticket") {
20539
+ const parts = [record["ticketNumber"], record["title"]].filter((v) => typeof v === "string" && v.trim() !== "").map((v) => v.trim());
20540
+ return parts.length > 0 ? parts.join(" ") : null;
19897
20541
  }
19898
- const toEndMs = toB.ms + DAY_MS - 1;
19899
- const filter = [
19900
- { field, op: "gte", value: isoUtc(fromB.ms) },
19901
- { field, op: "lte", value: isoUtc(toEndMs) }
19902
- ];
19903
- const res = await http.request({
19904
- method: "POST",
19905
- idempotent: true,
19906
- path,
19907
- body: { MaxRecords: limit, filter },
19908
- operation
19909
- });
19910
- return unwrapItems(res).items;
19911
- }
19912
-
19913
- // src/resources/special/billing_items.ts
19914
- async function windowQuery(_resource, kind, http, opts) {
19915
- const now = opts?.now ?? /* @__PURE__ */ new Date();
19916
- const tz = opts?.tz === void 0 ? void 0 : validateZone(opts.tz, `billing_items.${kind}`);
19917
- return dateWindowQuery(
19918
- http,
19919
- "/V1.0/BillingItems/query",
19920
- "itemDate",
19921
- kind,
19922
- clampWindowLimit(opts?.limit),
19923
- `billing_items.${kind}`,
19924
- now,
19925
- tz
19926
- );
20542
+ const value = record[FK_NAME_FIELD[table]];
20543
+ return typeof value === "string" && value.trim() !== "" ? value : null;
19927
20544
  }
19928
- async function customRange(_resource, from, to, http, opts) {
19929
- const zone = opts?.tz === void 0 ? void 0 : validateZone(opts.tz, "billing_items.customRange");
19930
- return customRangeQuery(
19931
- http,
19932
- "/V1.0/BillingItems/query",
19933
- "itemDate",
19934
- from,
19935
- to,
19936
- clampWindowLimit(opts?.limit),
19937
- "billing_items.customRange",
19938
- zone
19939
- );
20545
+ function faultRow(err) {
20546
+ const e = err;
20547
+ return e.httpStatus !== void 0 ? { code: e.code, message: e.message, httpStatus: e.httpStatus } : { code: e.code, message: e.message };
19940
20548
  }
19941
-
19942
- // src/resources/billing_items.ts
19943
- var BillingItemsResource = class extends BaseResource {
19944
- constructor(http, pageSize) {
19945
- super(http, {
19946
- key: "billing_items",
19947
- resourcePath: "/V1.0/BillingItems",
19948
- deletable: false,
19949
- udfCapable: false,
19950
- collectionUpdate: true
19951
- // D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
19952
- }, pageSize);
19953
- }
19954
- /** Query BillingItems records with the G3 filter surface (server-side JSON filter). */
19955
- query(params) {
19956
- return this.queryItems(params);
19957
- }
19958
- /** Get every BillingItems record (the MCP-preferred read; 1000-page guarded). */
19959
- async queryAll(params) {
19960
- return this.queryAllItems(params);
19961
- }
19962
- /** Count matching records (queryCount envelope unwrapped to a plain number). */
19963
- async count(params) {
19964
- return this.countGlobal(params);
19965
- }
19966
- /** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
19967
- async get(id, opts = {}) {
19968
- return this.getGlobal(id, opts?.fresh === true);
19969
- }
19970
- /** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
19971
- * keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
19972
- * partial body is merged into the CURRENT record first: one fresh GET (which also serves the
19973
- * stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
19974
- * caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
19975
- * the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
19976
- * `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
19977
- * wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
19978
- * mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
19979
- async update(id, data, opts = {}) {
19980
- if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
19981
- return this.updateGlobal(id, data, opts);
19982
- }
19983
- /** Resolve a BillingItems from a numeric id (or, where the vendor indexes a primary text field,
19984
- * from a numeric id — this resource has no primary text field). A numeric id miss throws NOT_FOUND;
19985
- * a bare value rejects with a config error (ids only);
19986
- * non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
19987
- * cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
19988
- * The compact shape is the structural alias of the full record; `expand: true` is the
19989
- * type-level escape hatch. */
19990
- async resolve(identifier, opts = {}) {
19991
- void opts;
19992
- return this.resolveBase(identifier);
19993
- }
19994
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
19995
- customRange(from, to, opts) {
19996
- return customRange(this, from, to, this.http, opts);
19997
- }
19998
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
19999
- last14(opts) {
20000
- return windowQuery(this, "last14", this.http, opts);
20001
- }
20002
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20003
- last24h(opts) {
20004
- return windowQuery(this, "last24h", this.http, opts);
20005
- }
20006
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20007
- last30(opts) {
20008
- return windowQuery(this, "last30", this.http, opts);
20009
- }
20010
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20011
- last7(opts) {
20012
- return windowQuery(this, "last7", this.http, opts);
20013
- }
20014
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20015
- last90(opts) {
20016
- return windowQuery(this, "last90", this.http, opts);
20017
- }
20018
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20019
- lastFullFortnight(opts) {
20020
- return windowQuery(this, "lastFullFortnight", this.http, opts);
20021
- }
20022
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20023
- lastFullMonth(opts) {
20024
- return windowQuery(this, "lastFullMonth", this.http, opts);
20025
- }
20026
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20027
- lastFullQuarter(opts) {
20028
- return windowQuery(this, "lastFullQuarter", this.http, opts);
20029
- }
20030
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20031
- lastFullWeek(opts) {
20032
- return windowQuery(this, "lastFullWeek", this.http, opts);
20549
+ function vendorId(value) {
20550
+ if (typeof value === "number") return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
20551
+ if (typeof value === "string" && /^[0-9]+$/.test(value.trim())) {
20552
+ const n = Number(value.trim());
20553
+ return Number.isSafeInteger(n) && n >= 1 ? n : void 0;
20033
20554
  }
20034
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20035
- today(opts) {
20036
- return windowQuery(this, "today", this.http, opts);
20555
+ return void 0;
20556
+ }
20557
+ async function resolveLineOfBusinessChain(http, associationId, options) {
20558
+ const operation = options.operation ?? "compositeNames";
20559
+ const degrade = options.onError === "degrade";
20560
+ const get = (path) => {
20561
+ const res = http.request({ method: "GET", path, operation }).then((body) => {
20562
+ const b = body;
20563
+ if (b === null || !("item" in b) || b.item === null || b.item === void 0) return null;
20564
+ return b.item;
20565
+ });
20566
+ if (!degrade) {
20567
+ return res.catch((e) => {
20568
+ if (e instanceof NotFoundError) return null;
20569
+ throw e;
20570
+ });
20571
+ }
20572
+ return res.catch((e) => {
20573
+ if (e instanceof NotFoundError || isAutotaskError(e) && e.code !== "ABORTED") return null;
20574
+ throw e;
20575
+ });
20576
+ };
20577
+ let remaining = options.budget;
20578
+ if (remaining < 1) {
20579
+ return {
20580
+ lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: "capped", organizationalLevel2: "capped", organizationalLevel1: "capped" } },
20581
+ used: 0,
20582
+ truncated: true
20583
+ };
20037
20584
  }
20038
- /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
20039
- yesterday(opts) {
20040
- return windowQuery(this, "yesterday", this.http, opts);
20585
+ remaining -= 1;
20586
+ const association = await get(`/V1.0/OrganizationalLevelAssociations/${associationId}`);
20587
+ const associationMarker = association === null ? "unresolved" : "ok";
20588
+ if (association === null) {
20589
+ return {
20590
+ lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: associationMarker, organizationalLevel2: "missing", organizationalLevel1: "missing" } },
20591
+ used: 1,
20592
+ truncated: false
20593
+ };
20041
20594
  }
20042
- };
20043
-
20044
- // src/operations/expandResourceNames.ts
20045
- var EXPAND_RESOURCE_NAMES_MAX_IDS = 500;
20046
- function composeName(record) {
20047
- const parts = [];
20048
- for (const field of ["firstName", "middleName", "lastName"]) {
20049
- const value = record[field];
20050
- if (typeof value === "string" && value.trim() !== "") parts.push(value.trim());
20595
+ const l2Id = vendorId(association.organizationalLevel2ID);
20596
+ const l1Id = vendorId(association.organizationalLevel1ID);
20597
+ let l2Marker = l2Id !== void 0 ? "unresolved" : "missing";
20598
+ let l1Marker = l1Id !== void 0 ? "unresolved" : "missing";
20599
+ const need = (l2Id !== void 0 ? 1 : 0) + (l1Id !== void 0 ? 1 : 0);
20600
+ let l2 = null;
20601
+ let l1 = null;
20602
+ let used = 1;
20603
+ let truncated = false;
20604
+ if (need > 0 && remaining < need) {
20605
+ truncated = true;
20606
+ if (l2Id !== void 0) l2Marker = "capped";
20607
+ if (l1Id !== void 0) l1Marker = "capped";
20608
+ } else {
20609
+ used += need;
20610
+ const [r2, r1] = await Promise.all([
20611
+ l2Id !== void 0 ? get(`/V1.0/OrganizationalLevel2s/${l2Id}`) : Promise.resolve(null),
20612
+ l1Id !== void 0 ? get(`/V1.0/OrganizationalLevel1s/${l1Id}`) : Promise.resolve(null)
20613
+ ]);
20614
+ l2 = r2;
20615
+ l1 = r1;
20616
+ if (l2Id !== void 0) l2Marker = l2 === null ? "unresolved" : "ok";
20617
+ if (l1Id !== void 0) l1Marker = l1 === null ? "unresolved" : "ok";
20051
20618
  }
20052
- return parts.length > 0 ? parts.join(" ") : null;
20619
+ return {
20620
+ lineOfBusiness: { association, organizationalLevel2: l2, organizationalLevel1: l1, markers: { association: associationMarker, organizationalLevel2: l2Marker, organizationalLevel1: l1Marker } },
20621
+ used,
20622
+ truncated
20623
+ };
20053
20624
  }
20054
- async function expandResourceNames(client, ids2, opts = {}) {
20055
- if (opts.maxIds !== void 0 && (!Number.isInteger(opts.maxIds) || opts.maxIds < 1)) {
20056
- throw new AutotaskConfigError(`expandResourceNames: maxIds must be an integer >= 1 (got ${String(opts.maxIds)})`);
20625
+ async function resolveCompositeNames(http, plan, opts = {}) {
20626
+ if (opts.maxNameReads !== void 0 && (!Number.isInteger(opts.maxNameReads) || opts.maxNameReads < 1)) {
20627
+ throw new AutotaskConfigError(`composite names: maxNameReads must be an integer >= 1 (got ${String(opts.maxNameReads)})`);
20057
20628
  }
20058
- const cap = Math.min(EXPAND_RESOURCE_NAMES_MAX_IDS, opts.maxIds ?? EXPAND_RESOURCE_NAMES_MAX_IDS);
20059
- if (!Array.isArray(ids2) || ids2.length === 0) {
20060
- throw new AutotaskConfigError(
20061
- "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)",
20062
- { suggestedAction: "Pass at least one vendor numeric resource id \u2014 safe integer, or the canonical integer string for ids above 2^53." }
20063
- );
20629
+ const budget = Math.min(DEFAULT_MAX_NAME_READS, opts.maxNameReads ?? DEFAULT_MAX_NAME_READS);
20630
+ const client = http.client;
20631
+ if (client === void 0) {
20632
+ throw new AutotaskConfigError("composite names: the transport is not wired to an AutotaskClient (the composite must run through the client)");
20064
20633
  }
20065
- if (ids2.length > cap) {
20066
- throw new AutotaskConfigError(
20067
- `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)`,
20068
- { suggestedAction: `Split the call into groups of <= ${cap} ids.` }
20069
- );
20634
+ const readCache = http.readCache;
20635
+ const nameCache = http.resourceNameCache;
20636
+ const section = { truncated: false, rows: {}, reasons: {} };
20637
+ let remaining = budget;
20638
+ let contractLobPointer;
20639
+ let contractLobUnread;
20640
+ const coldResource = [];
20641
+ for (const id of plan.resourceIds) {
20642
+ if (nameCache !== void 0 && nameCache.get(id) !== void 0) continue;
20643
+ if (readCache !== void 0 && readCache.get(`/V1.0/Resources/${id}`) !== void 0) continue;
20644
+ coldResource.push(id);
20070
20645
  }
20071
- const problems = [];
20072
- const canonical = [];
20073
- for (let i = 0; i < ids2.length; i += 1) {
20074
- const at = `ids[${i}]`;
20075
- const raw = ids2[i];
20076
- try {
20077
- canonical[i] = parseIdentifier(raw, at);
20078
- } catch {
20079
- if (typeof raw === "string") {
20080
- 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)`);
20081
- } else if (typeof raw === "number") {
20082
- 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)`);
20083
- } else {
20084
- problems.push(`${at}: required \u2014 the vendor numeric resource id (safe integer, or the canonical integer string for ids above 2^53)`);
20646
+ const withinBudget = coldResource.slice(0, remaining);
20647
+ const overBudgetResource = coldResource.slice(remaining);
20648
+ remaining -= withinBudget.length;
20649
+ for (const id of overBudgetResource) {
20650
+ section.rows[`resource:${id}`] = { name: null, blind: false, capped: true };
20651
+ section.truncated = true;
20652
+ }
20653
+ const overSet = new Set(overBudgetResource.map((id) => String(id)));
20654
+ const opIds = plan.resourceIds.filter((id) => !overSet.has(String(id)));
20655
+ if (opIds.length > 0) {
20656
+ let pass = opIds;
20657
+ if (pass.length > 500) {
20658
+ for (const id of pass.slice(500)) {
20659
+ section.rows[`resource:${id}`] = { name: null, blind: false, capped: true };
20660
+ section.truncated = true;
20085
20661
  }
20662
+ pass = pass.slice(0, 500);
20086
20663
  }
20087
- }
20088
- if (problems.length > 0) {
20089
- throw new AutotaskConfigError(`expandResourceNames: ${problems.length} problem(s) found pre-request (zero wire was issued)
20090
- - ` + problems.join("\n- "), {
20091
- 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)."
20092
- });
20093
- }
20094
- const unique = [];
20095
- const seen = /* @__PURE__ */ new Set();
20096
- for (const id of canonical) {
20097
- const k = String(id);
20098
- if (!seen.has(k)) {
20099
- seen.add(k);
20100
- unique.push(id);
20664
+ const res = await expandResourceNames(client, pass);
20665
+ for (const r of res.results) {
20666
+ const row = { name: r.name, blind: r.blind, capped: false };
20667
+ if (r.error !== void 0) row.error = faultRow(r.error);
20668
+ section.rows[`resource:${r.id}`] = row;
20101
20669
  }
20102
20670
  }
20103
- const holder = client.resources;
20104
- const cache = client.http.resourceNameCache;
20105
- const fresh = opts.fresh === true;
20106
- const byId = /* @__PURE__ */ new Map();
20107
- for (const id of unique) {
20108
- const k = String(id);
20109
- if (cache !== void 0 && !fresh) {
20110
- const shared = cache.inflight(id);
20111
- if (shared !== void 0) {
20112
- let joined;
20113
- try {
20114
- joined = await shared;
20115
- } catch (err) {
20116
- if (!isAutotaskError(err) || err.code !== "ABORTED") throw err;
20117
- }
20118
- if (joined !== void 0) {
20119
- byId.set(k, joined);
20120
- continue;
20671
+ const holders = {
20672
+ role: client.roles,
20673
+ billingCode: client.billingCodes,
20674
+ contract: client.contracts,
20675
+ company: client.companies,
20676
+ ticket: client.tickets
20677
+ };
20678
+ for (const fk of plan.recordFks) {
20679
+ const key = `${fk.table}:${fk.id}`;
20680
+ const cold = readCache === void 0 || readCache.get(`${FK_TABLE_PATH[fk.table]}/${fk.id}`) === void 0;
20681
+ if (cold) {
20682
+ if (remaining < 1) {
20683
+ section.rows[key] = { name: null, blind: false, capped: true };
20684
+ section.truncated = true;
20685
+ if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20686
+ contractLobUnread = "capped";
20121
20687
  }
20122
- }
20123
- const hit = cache.get(id);
20124
- if (hit !== void 0) {
20125
- byId.set(k, { name: hit.name, blind: hit.blind });
20126
20688
  continue;
20127
20689
  }
20690
+ remaining -= 1;
20128
20691
  }
20129
- const epoch = cache?.epoch ?? 0;
20130
- const work = (async () => {
20131
- try {
20132
- const record = await holder.get(id, { fresh });
20133
- const name = composeName(record);
20134
- if (cache !== void 0 && name !== null) cache.set(id, { name, blind: false }, epoch);
20135
- return { name, blind: false };
20136
- } catch (err) {
20137
- if (err instanceof NotFoundError) {
20138
- const entry = { name: null, blind: true };
20139
- if (cache !== void 0) cache.set(id, entry, epoch);
20140
- return { name: null, blind: true };
20141
- }
20142
- if (isAutotaskError(err) && err.code !== "ABORTED") {
20143
- const e = { code: err.code, message: err.message };
20144
- if (err.httpStatus !== void 0) e.httpStatus = err.httpStatus;
20145
- return { name: null, blind: false, error: e };
20692
+ try {
20693
+ const record = await holders[fk.table].get(fk.id);
20694
+ if (fk.table === "contract" && plan.lineOfBusinessId === void 0 && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20695
+ contractLobPointer = vendorId(record["organizationalLevelAssociationID"]);
20696
+ }
20697
+ section.rows[key] = { name: fkName(record, fk.table), blind: false, capped: false };
20698
+ } catch (err) {
20699
+ if (err instanceof NotFoundError) {
20700
+ section.rows[key] = { name: null, blind: true, capped: false };
20701
+ } else if (isAutotaskError(err) && err.code !== "ABORTED") {
20702
+ section.rows[key] = { name: null, blind: false, capped: false, error: faultRow(err) };
20703
+ if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20704
+ contractLobUnread = "unresolved";
20146
20705
  }
20706
+ } else {
20147
20707
  throw err;
20148
20708
  }
20149
- })();
20150
- const settled = cache !== void 0 ? await cache.track(id, work) : await work;
20151
- byId.set(k, settled);
20709
+ }
20152
20710
  }
20153
- const results = [];
20154
- for (const id of canonical) {
20155
- const out = byId.get(String(id));
20156
- results.push({ id, name: out.name, blind: out.blind, ...out.error !== void 0 ? { error: out.error } : {} });
20711
+ const picklists = {};
20712
+ if (plan.labelFields.length > 0) {
20713
+ let usable = true;
20714
+ try {
20715
+ const labelResource = plan.labelResource ?? "tickets";
20716
+ const catalog = await fieldCatalog(client, { resources: [labelResource], labels: true, fields: [...plan.labelFields] });
20717
+ const row = catalog.rows.find((r) => r.resource === labelResource);
20718
+ if (row === void 0 || row.error !== null || row.fields === void 0) {
20719
+ usable = false;
20720
+ } else {
20721
+ for (const f of plan.labelFields) {
20722
+ const frow = row.fields.find((x) => x.name === f);
20723
+ picklists[f] = frow?.picklistValues;
20724
+ }
20725
+ }
20726
+ } catch (err) {
20727
+ if (isAutotaskError(err) && err.code !== "ABORTED") usable = false;
20728
+ else throw err;
20729
+ }
20730
+ if (!usable) for (const f of plan.labelFields) section.reasons[f] = FIELD_CATALOG_HONESTY_NOTE;
20157
20731
  }
20158
- const resolved = results.filter((r) => r.name !== null).length;
20159
- const blind = results.filter((r) => r.blind === true).length;
20160
- const failed = results.filter((r) => r.error !== void 0).length;
20161
- return { results, resolved, blind, failed };
20732
+ let lineOfBusiness2 = null;
20733
+ const lobId = plan.lineOfBusinessId ?? contractLobPointer;
20734
+ if (lobId !== void 0) {
20735
+ const chain = await resolveLineOfBusinessChain(http, lobId, { budget: remaining, onError: "degrade" });
20736
+ remaining -= chain.used;
20737
+ if (chain.truncated) section.truncated = true;
20738
+ const fromContract = plan.lineOfBusinessId === void 0;
20739
+ lineOfBusiness2 = {
20740
+ ...chain.lineOfBusiness,
20741
+ ...fromContract ? { source: "contract", contractId: plan.lineOfBusinessContractId } : plan.lineOfBusinessSource !== void 0 ? { source: plan.lineOfBusinessSource } : {},
20742
+ ...!fromContract && plan.lineOfBusinessContractId !== void 0 ? { contractId: plan.lineOfBusinessContractId } : {}
20743
+ };
20744
+ } else if (contractLobUnread !== void 0 && plan.lineOfBusinessContractId !== void 0) {
20745
+ lineOfBusiness2 = {
20746
+ source: "contract",
20747
+ contractId: plan.lineOfBusinessContractId,
20748
+ association: null,
20749
+ organizationalLevel2: null,
20750
+ organizationalLevel1: null,
20751
+ markers: { association: contractLobUnread, organizationalLevel2: "missing", organizationalLevel1: "missing" }
20752
+ };
20753
+ }
20754
+ return {
20755
+ section,
20756
+ lineOfBusiness: lineOfBusiness2,
20757
+ resourceName: (id) => section.rows[`resource:${id}`]?.name ?? null,
20758
+ recordName: (table, id) => section.rows[`${table}:${id}`]?.name ?? null,
20759
+ labelFor: (field, value) => {
20760
+ if (value === null || value === void 0) return null;
20761
+ const values = picklists[field];
20762
+ if (values === void 0) {
20763
+ section.reasons[field] = FIELD_CATALOG_HONESTY_NOTE;
20764
+ return null;
20765
+ }
20766
+ const hit = values.find((v) => String(v.value) === String(value));
20767
+ if (hit === void 0) {
20768
+ section.reasons[field] = PICKLIST_VALUE_NOT_SERVED_NOTE;
20769
+ return null;
20770
+ }
20771
+ return typeof hit.label === "string" ? hit.label : null;
20772
+ }
20773
+ };
20162
20774
  }
20163
-
20164
- // src/operations/entityType.ts
20165
- function buildEntityTypeMap() {
20166
- const exact = {};
20167
- for (const [resource, entry] of Object.entries(RELATIONSHIP_MAP)) {
20168
- const apiEntity = entry.apiEntity;
20169
- if (typeof apiEntity !== "string" || apiEntity === "") continue;
20170
- if (exact[apiEntity] === void 0) exact[apiEntity] = resource;
20171
- }
20172
- const aliases = {};
20173
- for (const [apiEntity, resource] of Object.entries(exact)) {
20174
- if (!apiEntity.endsWith("s") || apiEntity.endsWith("ss")) continue;
20175
- const singular = apiEntity.slice(0, -1);
20176
- if (exact[singular] !== void 0 || aliases[singular] !== void 0) continue;
20177
- aliases[singular] = resource;
20775
+ var ROW_FK_TABLES = {
20776
+ companyID: "company",
20777
+ contractID: "contract",
20778
+ ticketID: "ticket",
20779
+ roleID: "role",
20780
+ billingCodeID: "billingCode"
20781
+ };
20782
+ var isResourceFkField = (field) => field === "resourceID" || field.endsWith("ResourceID");
20783
+ var LABEL_NAME_MAX_CHARS = 200;
20784
+ function untrustedName(name) {
20785
+ if (name === null) return null;
20786
+ const [[capped]] = truncateNoteChars([{ v: name }], LABEL_NAME_MAX_CHARS, ["v"]);
20787
+ return wrapUntrusted(capped.v) ?? capped.v;
20788
+ }
20789
+ function readNamesOpt(value, where) {
20790
+ if (value === void 0) return true;
20791
+ if (typeof value !== "boolean") {
20792
+ throw new AutotaskConfigError(`${where}: names must be a boolean (got ${typeof value}) \u2014 true (default) adds label siblings, false returns raw ids only`);
20178
20793
  }
20179
- return { ...exact, ...aliases };
20180
- }
20181
- var ENTITY_TYPE_TO_RESOURCE = buildEntityTypeMap();
20182
- var ENUMERABLE_RESOURCES = new Set(
20183
- CAPABILITIES.filter((rec) => rec.resource !== "operations" && (rec.operation === "query" || rec.operation === "get")).map(
20184
- (rec) => rec.resource
20185
- )
20186
- );
20187
- function resourceForEntityType(entityType) {
20188
- if (typeof entityType !== "string" || entityType === "") return null;
20189
- return ENTITY_TYPE_TO_RESOURCE[entityType] ?? null;
20190
- }
20191
- function isEnumerableResource(resource) {
20192
- if (typeof resource !== "string" || resource === "") return false;
20193
- return ENUMERABLE_RESOURCES.has(resource);
20794
+ return value;
20194
20795
  }
20195
-
20196
- // src/operations/types.ts
20197
- var WORKFLOW_RESOURCES = [
20198
- "companies",
20199
- "tickets",
20200
- "contacts",
20201
- "configuration_items",
20202
- "projects"
20203
- ];
20204
-
20205
- // src/operations/fieldCatalog.ts
20206
- var MAX_FIELD_CATALOG_WIRE = 60;
20207
- 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.";
20208
- var FIELD_CATALOG_OPT_KEYS = ["resources", "include", "fresh", "fields", "labels"];
20209
- function projectFieldRow(raw, labels = false, udf = false) {
20210
- const r = typeof raw === "object" && raw !== null ? raw : {};
20211
- const name = typeof r.name === "string" ? r.name : "";
20212
- const type = typeof r.dataType === "string" ? r.dataType : typeof r.type === "string" ? r.type : "";
20213
- const values = labels && r.isPickList === true && Array.isArray(r.picklistValues) && r.picklistValues.length > 0 ? r.picklistValues : void 0;
20214
- const referenceEntityType = typeof r.referenceEntityType === "string" ? r.referenceEntityType : "";
20215
- const targetResource = resourceForEntityType(referenceEntityType);
20796
+ async function labelRows(client, resourceKey, rows) {
20797
+ const present = /* @__PURE__ */ new Set();
20798
+ for (const row of rows) for (const [k, v] of Object.entries(row)) if (v !== null && v !== void 0) present.add(k);
20799
+ if (present.size === 0) return { rows: [...rows] };
20800
+ let picklistFields = [];
20801
+ const modelFields = MODEL_FIELD_SETS[resourceKey] ?? [];
20802
+ const candidates = [...present].filter((f) => f !== "id" && modelFields.includes(f) && ROW_FK_TABLES[f] === void 0 && !isResourceFkField(f));
20803
+ if (candidates.length > 0) try {
20804
+ const catalog = await fieldCatalog(client, { resources: [resourceKey], labels: true });
20805
+ const fields = catalog.rows.find((r) => r.resource === resourceKey)?.fields ?? [];
20806
+ picklistFields = fields.filter((f) => f.isPickList && candidates.includes(f.name)).map((f) => f.name);
20807
+ } catch (err) {
20808
+ if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
20809
+ }
20810
+ const builder = createNamePlan(picklistFields, resourceKey);
20811
+ for (const row of rows) {
20812
+ for (const [field, value] of Object.entries(row)) {
20813
+ if (value === 0 || value === "0") continue;
20814
+ const table = ROW_FK_TABLES[field];
20815
+ if (table !== void 0) builder.addFk(table, value);
20816
+ else if (isResourceFkField(field)) builder.addResource(value);
20817
+ }
20818
+ }
20819
+ const plan = builder.plan();
20820
+ if (plan.resourceIds.length === 0 && plan.recordFks.length === 0 && picklistFields.length === 0) return { rows: [...rows] };
20821
+ const names = await resolveCompositeNames(client.http, plan);
20822
+ const out = rows.map((row) => {
20823
+ const r = { ...row };
20824
+ for (const [field, value] of Object.entries(row)) {
20825
+ if (value === null || value === void 0 || value === 0 || value === "0") continue;
20826
+ if (picklistFields.includes(field)) r[`${field}Label`] = names.labelFor(field, value);
20827
+ const table = ROW_FK_TABLES[field];
20828
+ if (table !== void 0 && isVendorId(value)) r[`${field}Name`] = untrustedName(names.recordName(table, value));
20829
+ else if (isResourceFkField(field) && isVendorId(value)) r[`${field}Name`] = untrustedName(names.resourceName(value));
20830
+ }
20831
+ if (r["assignedResourceIDName"] !== void 0) r["assignedResourceName"] = r["assignedResourceIDName"];
20832
+ return r;
20833
+ });
20834
+ const capped = Object.values(names.section.rows).filter((row) => row.capped).length;
20835
+ const reasons = names.section.reasons;
20836
+ if (capped === 0 && Object.keys(reasons).length === 0) return { rows: out };
20837
+ const parts = [];
20838
+ if (capped > 0) parts.push(`${capped} id(s) 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`);
20839
+ if (Object.keys(reasons).length > 0) parts.push(`picklist label(s) unavailable for ${Object.keys(reasons).join(", ")} (see reasons)`);
20216
20840
  return {
20217
- name,
20218
- ...typeof r.label === "string" && r.label !== "" ? { label: r.label } : {},
20219
- type,
20220
- isRequired: r.isRequired === true,
20221
- isReadOnly: r.isReadOnly === true,
20222
- // Issue #219 N7: a UDF row never serves the vendor's isQueryable (it is
20223
- // true for UDFs while a plain filter on a UDF name is refused); it serves
20224
- // the marker the filter actually needs.
20225
- ...udf ? { filterable: "udf-marker" } : { isQueryable: r.isQueryable === true },
20226
- isPickList: r.isPickList === true,
20227
- ...isObjectOrArrayType(type) ? { writeShape: "replace-all" } : {},
20228
- maxLength: typeof r.length === "number" ? r.length : null,
20229
- referenceEntityType,
20230
- targetResource,
20231
- targetEnumerable: isEnumerableResource(targetResource),
20232
- ...values !== void 0 ? { picklistValues: values } : {}
20841
+ rows: out,
20842
+ names: { truncated: capped > 0, capped, note: parts.join("; "), ...Object.keys(reasons).length > 0 ? { reasons } : {} }
20233
20843
  };
20234
20844
  }
20235
- function isObjectOrArrayType(type) {
20236
- const t = type.trim().toLowerCase();
20237
- return t === "object" || t === "array" || t === "collection" || t.endsWith("[]");
20238
- }
20239
- function exposesPicklistValues(raw) {
20240
- const r = typeof raw === "object" && raw !== null ? raw : {};
20241
- return Array.isArray(r.picklistValues) && r.picklistValues.length > 0;
20845
+ async function labelFetchResults(client, result) {
20846
+ const byResource = /* @__PURE__ */ new Map();
20847
+ result.results.forEach((r, i) => {
20848
+ if (r.found && r.value !== null) byResource.set(r.resource, [...byResource.get(r.resource) ?? [], i]);
20849
+ });
20850
+ const results = [...result.results];
20851
+ const summaries = [];
20852
+ for (const [resource, indices] of byResource) {
20853
+ const labelled = await labelRows(client, resource, indices.map((i) => results[i].value));
20854
+ indices.forEach((idx, j) => {
20855
+ results[idx] = { ...results[idx], value: labelled.rows[j] };
20856
+ });
20857
+ if (labelled.names !== void 0) summaries.push(labelled.names);
20858
+ }
20859
+ if (summaries.length === 0) return { ...result, results };
20860
+ const reasons = Object.assign({}, ...summaries.map((s) => s.reasons ?? {}));
20861
+ const names = {
20862
+ truncated: summaries.some((s) => s.truncated),
20863
+ capped: summaries.reduce((n, s) => n + s.capped, 0),
20864
+ note: summaries.map((s) => s.note).join("; "),
20865
+ ...Object.keys(reasons).length > 0 ? { reasons } : {}
20866
+ };
20867
+ return { ...result, results, names };
20242
20868
  }
20243
- function applyFieldsFilter(projected, filter, row) {
20244
- const kept = filter === void 0 ? projected : projected.filter((f) => filter.includes(f.name));
20245
- if (filter !== void 0) {
20246
- const present = new Set(projected.map((f) => f.name));
20247
- row.unmatchedFields = filter.filter((n) => !present.has(n));
20248
- row.projected = true;
20869
+
20870
+ // src/resources/billing_codes.ts
20871
+ var BillingCodesResource = class extends BaseResource {
20872
+ constructor(http, pageSize) {
20873
+ super(http, {
20874
+ key: "billing_codes",
20875
+ resourcePath: "/V1.0/BillingCodes",
20876
+ deletable: false,
20877
+ udfCapable: false,
20878
+ searchFields: ["name"]
20879
+ }, pageSize);
20249
20880
  }
20250
- row.fields = kept;
20251
- row.required = kept.filter((f) => f.isRequired).map((f) => f.name);
20252
- row.readOnly = kept.filter((f) => f.isReadOnly).map((f) => f.name);
20253
- row.queryable = kept.filter((f) => f.isQueryable).map((f) => f.name);
20254
- row.picklistFields = kept.filter((f) => f.isPickList).map((f) => f.name);
20255
- }
20256
- function capabilitySets() {
20257
- const fieldOps = /* @__PURE__ */ new Map();
20258
- const udfCapable = /* @__PURE__ */ new Set();
20259
- const resources = /* @__PURE__ */ new Set();
20260
- for (const rec of CAPABILITIES) {
20261
- if (rec.resource === "operations") continue;
20262
- resources.add(rec.resource);
20263
- if (rec.operation === "getFieldInfo") fieldOps.set(rec.resource, rec);
20264
- if (rec.udfCapable) udfCapable.add(rec.resource);
20881
+ /** Query BillingCode records with the G3 filter surface (server-side JSON filter). */
20882
+ query(params) {
20883
+ return this.queryItems(params);
20265
20884
  }
20266
- return { fieldOps, udfCapable, resources };
20267
- }
20268
- var derivedIndex = null;
20269
- function derivedFieldsFor(resource) {
20270
- if (derivedIndex === null) {
20271
- derivedIndex = /* @__PURE__ */ new Map();
20272
- for (const rec of CAPABILITIES) {
20273
- const declared = rec.metadata?.derivedFields;
20274
- if (!Array.isArray(declared) || derivedIndex.has(rec.resource)) continue;
20275
- const rows = [];
20276
- for (const row of declared) {
20277
- if (typeof row?.field !== "string" || typeof row.rule !== "string") continue;
20278
- rows.push({
20279
- field: row.field,
20280
- rule: row.rule,
20281
- ...typeof row.writableRoute === "string" ? { writableRoute: row.writableRoute } : {}
20282
- });
20283
- }
20284
- derivedIndex.set(rec.resource, rows);
20285
- }
20885
+ /** Get every BillingCode record (the MCP-preferred read; 1000-page guarded). */
20886
+ async queryAll(params) {
20887
+ return this.queryAllItems(params);
20286
20888
  }
20287
- return derivedIndex.get(resource) ?? [];
20288
- }
20289
- function apiEntityFor(rec) {
20290
- const m = /^GET \/V1\.0\/([^/]+)\/entityInformation\/fields$/.exec(rec.endpoint ?? "");
20291
- if (m === null) {
20292
- throw new AutotaskConfigError(
20293
- `fieldCatalog: the plan's getFieldInfo endpoint for "${rec.resource}" has no /V1.0/<Entity>/entityInformation/fields shape (${rec.endpoint}) \u2014 plan defect`,
20294
- { operation: "operations.fieldCatalog" }
20295
- );
20889
+ /** Count matching records (queryCount envelope unwrapped to a plain number). */
20890
+ async count(params) {
20891
+ return this.countGlobal(params);
20296
20892
  }
20297
- return m[1];
20298
- }
20299
- function validateCatalogOptsShape(opts) {
20300
- if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
20301
- throw new AutotaskConfigError(
20302
- "fieldCatalog: opts must be an object { resources?: string[], include?: (info|fields|udf)[], fresh?: boolean, fields?: string[], labels?: boolean }"
20303
- );
20893
+ /** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
20894
+ async get(id, opts = {}) {
20895
+ return this.getGlobal(id, opts?.fresh === true);
20304
20896
  }
20305
- for (const key of Object.keys(opts ?? {})) {
20306
- if (!FIELD_CATALOG_OPT_KEYS.includes(key)) {
20307
- throw new AutotaskConfigError(
20308
- `fieldCatalog: unknown opts key "${key}" \u2014 the bag is closed: ${FIELD_CATALOG_OPT_KEYS.join(", ")}`,
20309
- { suggestedAction: "Drop the unknown key (autotask_describe(operations.fieldCatalog) shows the exact bag)." }
20310
- );
20311
- }
20897
+ /** Resolve a BillingCode from a numeric id (or, where the vendor indexes a primary text field,
20898
+ * from an exact match on name). A numeric id miss throws NOT_FOUND; a complete
20899
+ * non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
20900
+ * cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
20901
+ * The compact shape is the structural alias of the full record; `expand: true` is the
20902
+ * type-level escape hatch. */
20903
+ async resolve(identifier, opts = {}) {
20904
+ void opts;
20905
+ return this.resolveBase(identifier);
20312
20906
  }
20313
- }
20314
- function resolveInclude(opts) {
20315
- const includeRaw = opts?.include ?? ["fields"];
20316
- if (!Array.isArray(includeRaw) || includeRaw.length === 0) {
20317
- throw new AutotaskConfigError("fieldCatalog: opts.include must be a non-empty array of (info|fields|udf)");
20907
+ /** Search BillingCode records: bounded contains on name (no vendor free-text search exists);
20908
+ * limit defaults to 25, max 500. */
20909
+ async search(query, opts) {
20910
+ return this.searchBase(query, { limit: opts?.limit });
20318
20911
  }
20319
- const include = [];
20320
- for (const kind of includeRaw) {
20321
- if (typeof kind !== "string" || !FIELD_CACHE_KINDS.includes(kind)) {
20322
- throw new AutotaskConfigError(
20323
- `fieldCatalog: unknown include kind "${String(kind)}" \u2014 the valid kinds are: ${FIELD_CACHE_KINDS.join(", ")}`,
20324
- { suggestedAction: `Filter with one or more of: ${FIELD_CACHE_KINDS.join(", ")} (default ['fields']).` }
20325
- );
20326
- }
20327
- if (!include.includes(kind)) include.push(kind);
20912
+ };
20913
+
20914
+ // src/resources/special/dateWindow.ts
20915
+ var DAY_MS = 864e5;
20916
+ var DEFAULT_LIMIT = 500;
20917
+ var MAX_LIMIT = 500;
20918
+ function clampWindowLimit(limit) {
20919
+ const n = limit === void 0 ? DEFAULT_LIMIT : Math.trunc(limit);
20920
+ if (!Number.isInteger(n) || n < 1) {
20921
+ throw new AutotaskConfigError(`limit must be an integer >= 1 (got ${String(limit)})`);
20328
20922
  }
20329
- return include;
20923
+ return Math.min(n, MAX_LIMIT);
20330
20924
  }
20331
- function resolveTargets(opts, fieldOps, allResources) {
20332
- const resourcesRaw = opts?.resources ?? WORKFLOW_RESOURCES;
20333
- if (!Array.isArray(resourcesRaw) || resourcesRaw.length === 0) {
20334
- throw new AutotaskConfigError("fieldCatalog: opts.resources must be a non-empty array of plan resource keys");
20335
- }
20336
- const targets = [];
20337
- for (const r of resourcesRaw) {
20338
- if (typeof r !== "string" || r.trim() === "") {
20339
- throw new AutotaskConfigError(
20340
- `fieldCatalog: opts.resources must contain non-empty plan resource key strings (got ${typeof r === "string" && r.trim() === "" ? "a blank string" : typeof r})`
20341
- );
20342
- }
20343
- const key = r.trim();
20344
- const fieldOp = fieldOps.get(key);
20345
- if (fieldOp === void 0) {
20346
- if (allResources.has(key)) {
20347
- throw new AutotaskConfigError(
20348
- `fieldCatalog: resource "${key}" has no field catalog \u2014 the vendor defines the entityInformation trio for the ${fieldOps.size} plan entity resources only`,
20349
- { suggestedAction: `Pass one of the ${fieldOps.size} field-capable plan keys (list them with autotask_catalog).` }
20350
- );
20351
- }
20352
- throw new AutotaskError(
20353
- `fieldCatalog: unknown resource "${key}" \u2014 the plan has ${fieldOps.size} field-capable resources`,
20354
- {
20355
- code: "RESOURCE_UNKNOWN",
20356
- category: "validation",
20357
- suggestedAction: "Pass the exact plan resource key (snake_case) from autotask_catalog (e.g. tickets, companies, configuration_items)."
20358
- }
20359
- );
20925
+ var iso = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
20926
+ var WEEKDAYS = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
20927
+ function wallParts(tz, ms) {
20928
+ const parts = new Intl.DateTimeFormat("en-US", {
20929
+ timeZone: tz,
20930
+ year: "numeric",
20931
+ month: "2-digit",
20932
+ day: "2-digit",
20933
+ hour: "2-digit",
20934
+ minute: "2-digit",
20935
+ second: "2-digit",
20936
+ hour12: false,
20937
+ weekday: "short"
20938
+ }).formatToParts(new Date(ms));
20939
+ const w = {};
20940
+ let weekday = -1;
20941
+ for (const p of parts) {
20942
+ switch (p.type) {
20943
+ case "year":
20944
+ w.y = Number(p.value);
20945
+ break;
20946
+ case "month":
20947
+ w.m = Number(p.value);
20948
+ break;
20949
+ case "day":
20950
+ w.d = Number(p.value);
20951
+ break;
20952
+ case "hour":
20953
+ w.h = Number(p.value) % 24;
20954
+ break;
20955
+ case "minute":
20956
+ w.mi = Number(p.value);
20957
+ break;
20958
+ case "second":
20959
+ w.s = Number(p.value);
20960
+ break;
20961
+ case "weekday":
20962
+ weekday = WEEKDAYS[p.value] ?? -1;
20963
+ break;
20360
20964
  }
20361
- if (!targets.includes(key)) targets.push(key);
20362
20965
  }
20363
- return targets;
20364
- }
20365
- function resolveFieldsFilter(opts) {
20366
- const fieldsRaw = opts?.fields;
20367
- if (fieldsRaw === void 0) return void 0;
20368
- if (!Array.isArray(fieldsRaw) || fieldsRaw.length === 0) {
20369
- throw new AutotaskConfigError("fieldCatalog: opts.fields must be a non-empty array of vendor field name strings");
20966
+ if (weekday < 0 || [w.y, w.m, w.d, w.h, w.mi, w.s].some((v) => v === void 0 || Number.isNaN(v))) {
20967
+ throw new AutotaskConfigError(`dateWindow: could not read the wall-clock parts for timezone "${tz}" (the Intl format returned an incomplete or unmapped part)`);
20370
20968
  }
20371
- const names = [];
20372
- for (const f of fieldsRaw) {
20373
- if (typeof f !== "string" || f.trim() === "") {
20374
- throw new AutotaskConfigError(
20375
- `fieldCatalog: opts.fields must contain non-empty vendor field name strings (got ${typeof f === "string" && f.trim() === "" ? "a blank string" : typeof f})`
20376
- );
20377
- }
20378
- const name = f.trim();
20379
- if (!names.includes(name)) names.push(name);
20969
+ return { y: w.y, m: w.m, d: w.d, h: w.h, mi: w.mi, s: w.s, weekday };
20970
+ }
20971
+ function tzOffsetMs(tz, ms) {
20972
+ const p = wallParts(tz, ms);
20973
+ return Date.UTC(p.y, p.m - 1, p.d, p.h, p.mi, p.s) - ms;
20974
+ }
20975
+ function localDayStartMs(y, m, d, tz) {
20976
+ if (tz === void 0) return Date.UTC(y, m - 1, d);
20977
+ const candidate2 = Date.UTC(y, m - 1, d);
20978
+ const offset = tzOffsetMs(tz, candidate2);
20979
+ let start = candidate2 - offset;
20980
+ if (tzOffsetMs(tz, start) !== offset) {
20981
+ start = candidate2 - tzOffsetMs(tz, start);
20380
20982
  }
20381
- return names;
20983
+ const p = wallParts(tz, start);
20984
+ if (p.d !== d) start += 864e5 - ((p.h * 60 + p.mi) * 60 + p.s) * 1e3;
20985
+ return start;
20382
20986
  }
20383
- function resolveLabels(opts) {
20384
- if (opts !== void 0 && opts.labels !== void 0 && typeof opts.labels !== "boolean") {
20987
+ function localDayEndMs(y, m, d, tz) {
20988
+ if (tz === void 0) return Date.UTC(y, m - 1, d) + DAY_MS - 1;
20989
+ const next = new Date(Date.UTC(y, m - 1, d + 1));
20990
+ return localDayStartMs(next.getUTCFullYear(), next.getUTCMonth() + 1, next.getUTCDate(), tz) - 1;
20991
+ }
20992
+ function validateZone(tz, opName = "dateWindow") {
20993
+ if (typeof tz !== "string" || tz.length === 0) {
20385
20994
  throw new AutotaskConfigError(
20386
- `fieldCatalog: opts.labels must be a boolean (got ${typeof opts.labels})`
20995
+ `${opName}: tz must be a non-empty IANA timezone string (got ${JSON.stringify(tz)}; e.g. 'Australia/Sydney' \u2014 omitted tz = UTC)`,
20996
+ { operation: opName }
20387
20997
  );
20388
20998
  }
20389
- return opts?.labels === true;
20390
- }
20391
- function assertWithinWireCap(targetCount, kindCount) {
20392
- const cost = targetCount * kindCount;
20393
- if (cost > MAX_FIELD_CATALOG_WIRE) {
20999
+ try {
21000
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
21001
+ } catch {
20394
21002
  throw new AutotaskConfigError(
20395
- `fieldCatalog: ${targetCount} resource(s) \xD7 ${kindCount} kind(s) = ${cost} metadata reads exceed the wire cap of ${MAX_FIELD_CATALOG_WIRE} \u2014 the call is refused above its cap (never truncated)`,
20396
- { suggestedAction: `Pass a smaller resources list or a narrower include set so the product stays \u2264 ${MAX_FIELD_CATALOG_WIRE}.` }
21003
+ `${opName}: tz is not a valid IANA timezone (${JSON.stringify(tz)}; e.g. 'Australia/Sydney' \u2014 omitted tz = UTC)`,
21004
+ { operation: opName }
20397
21005
  );
20398
21006
  }
21007
+ return tz;
20399
21008
  }
20400
- function kindFetches(holder, resource) {
20401
- return {
20402
- info: () => holder?.getEntityInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getEntityInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" })),
20403
- fields: () => holder?.getFieldInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getFieldInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" })),
20404
- udf: () => holder?.getUdfInfo?.() ?? Promise.reject(new AutotaskError(`fieldCatalog: the client exposes no getUdfInfo read for "${resource}"`, { code: "INVALID_RESPONSE", category: "server" }))
20405
- };
21009
+ var utcDayStart = (ms) => {
21010
+ const d = new Date(ms);
21011
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
21012
+ };
21013
+ var utcDayEnd = (ms) => utcDayStart(ms) + DAY_MS - 1;
21014
+ function utcDateWindowBounds(kind, now) {
21015
+ const t = now.getTime();
21016
+ let from;
21017
+ let to;
21018
+ switch (kind) {
21019
+ case "today":
21020
+ from = utcDayStart(t);
21021
+ to = utcDayEnd(t);
21022
+ break;
21023
+ case "yesterday":
21024
+ from = utcDayStart(t - DAY_MS);
21025
+ to = utcDayEnd(t - DAY_MS);
21026
+ break;
21027
+ case "last24h":
21028
+ from = t - DAY_MS;
21029
+ to = t;
21030
+ break;
21031
+ case "last7":
21032
+ case "last14":
21033
+ case "last30":
21034
+ case "last90": {
21035
+ const days = Number(kind.slice(4));
21036
+ from = utcDayStart(t - days * DAY_MS);
21037
+ to = utcDayEnd(t);
21038
+ break;
21039
+ }
21040
+ case "lastFullWeek":
21041
+ case "lastFullFortnight": {
21042
+ const d = new Date(t);
21043
+ const daysSinceMonday = (d.getUTCDay() + 6) % 7;
21044
+ const thisMonday = utcDayStart(t) - daysSinceMonday * DAY_MS;
21045
+ const daysBack = kind === "lastFullWeek" ? 7 : 14;
21046
+ from = thisMonday - daysBack * DAY_MS;
21047
+ to = thisMonday - 1;
21048
+ break;
21049
+ }
21050
+ case "lastFullMonth": {
21051
+ const d = new Date(t);
21052
+ const y = d.getUTCFullYear();
21053
+ const m = d.getUTCMonth() - 1;
21054
+ from = Date.UTC(y, m, 1);
21055
+ to = Date.UTC(y, m + 1, 1) - 1;
21056
+ break;
21057
+ }
21058
+ case "lastFullQuarter": {
21059
+ const d = new Date(t);
21060
+ let q = Math.floor(d.getUTCMonth() / 3) - 1;
21061
+ let y = d.getUTCFullYear();
21062
+ if (q < 0) {
21063
+ q = 3;
21064
+ y -= 1;
21065
+ }
21066
+ const first = q * 3;
21067
+ from = Date.UTC(y, first, 1);
21068
+ to = Date.UTC(y, first + 3, 1) - 1;
21069
+ break;
21070
+ }
21071
+ }
21072
+ return { from: iso(from), to: iso(to) };
20406
21073
  }
20407
- async function sweepResource(ctx, resource, tally) {
20408
- const { client, include, fieldsFilter, labels, fresh } = ctx;
20409
- const cache = client.http.fieldCache;
20410
- const fieldOp = ctx.fieldOps.get(resource);
20411
- if (fieldOp === void 0) return void 0;
20412
- const apiEntity = apiEntityFor(fieldOp);
20413
- const holder = client[camelKey(resource)];
20414
- const row = {
20415
- resource,
20416
- apiEntity,
20417
- udfCapable: ctx.udfCapable.has(resource),
20418
- // Issue #219 N7: the UDF surface is declared with the mechanism, not a boolean.
20419
- ...ctx.udfCapable.has(resource) ? { udfFilter: "udf-marker", udfWriteShape: "merge" } : {},
20420
- unmatchedFields: [],
20421
- required: [],
20422
- readOnly: [],
20423
- queryable: [],
20424
- picklistFields: [],
20425
- cache: { hit: false, age: null },
20426
- wire: 0,
20427
- error: null
21074
+ function dateWindowBounds(kind, now, tz) {
21075
+ if (tz === void 0) return utcDateWindowBounds(kind, now);
21076
+ const zone = validateZone(tz);
21077
+ const t = now.getTime();
21078
+ if (kind === "last24h") {
21079
+ return { from: iso(t - DAY_MS), to: iso(t) };
21080
+ }
21081
+ const { y, m, d, weekday } = wallParts(zone, t);
21082
+ const shifted = (dy) => new Date(Date.UTC(y, m - 1, d + dy));
21083
+ const dayStart = (dy) => {
21084
+ const dt = shifted(dy);
21085
+ return localDayStartMs(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate(), zone);
20428
21086
  };
20429
- const fetches = kindFetches(holder, resource);
20430
- if (fresh) cache?.invalidate(resource);
20431
- const projectList = (kind, list) => {
20432
- const projected = list.map((f) => projectFieldRow(f, labels, kind === "udf"));
20433
- if (kind === "fields") applyFieldsFilter(projected, fieldsFilter, row);
20434
- else row.udf = projected;
20435
- for (const f of list) if (exposesPicklistValues(f)) tally.anyPicklistValues = true;
21087
+ const dayStartAt = (instant) => {
21088
+ const p = wallParts(zone, instant);
21089
+ return localDayStartMs(p.y, p.m, p.d, zone);
20436
21090
  };
20437
- let allCached = true;
20438
- const ages = [];
20439
- for (const kind of include) {
20440
- const peek = cache !== void 0 && !fresh ? cache.peek(resource, kind) : { hit: false, age: null };
20441
- if (peek.hit !== true) allCached = false;
20442
- if (peek.hit === true && cache !== void 0) {
20443
- const value2 = cache.get(resource, kind);
20444
- if (kind === "info") row.info = value2 ?? null;
20445
- else projectList(kind, Array.isArray(value2) ? value2 : []);
20446
- if (peek.age !== null) ages.push(peek.age);
20447
- continue;
20448
- }
20449
- let value;
20450
- try {
20451
- value = await fetches[kind]();
20452
- } catch (err) {
20453
- if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
20454
- tally.wireUsed += 1;
20455
- row.wire += 1;
20456
- if (row.error === null) row.error = { code: err.code, message: err.message };
20457
- continue;
21091
+ const dayEndAt = (instant) => {
21092
+ const p = wallParts(zone, instant);
21093
+ return localDayEndMs(p.y, p.m, p.d, zone);
21094
+ };
21095
+ let from;
21096
+ let to;
21097
+ switch (kind) {
21098
+ case "today":
21099
+ from = dayStartAt(t);
21100
+ to = dayEndAt(t);
21101
+ break;
21102
+ case "yesterday":
21103
+ from = dayStart(-1);
21104
+ to = dayStart(0) - 1;
21105
+ break;
21106
+ case "last7":
21107
+ case "last14":
21108
+ case "last30":
21109
+ case "last90": {
21110
+ const days = Number(kind.slice(4));
21111
+ from = dayStart(-days);
21112
+ to = dayEndAt(t);
21113
+ break;
20458
21114
  }
20459
- tally.wireUsed += 1;
20460
- row.wire += 1;
20461
- if (kind === "info") {
20462
- row.info = value ?? null;
20463
- } else {
20464
- if (!Array.isArray(value)) {
20465
- if (row.error === null) row.error = { code: "INVALID_RESPONSE", message: `fieldCatalog: the ${kind} response for ${resource} was not a list` };
20466
- continue;
20467
- }
20468
- projectList(kind, value);
21115
+ case "lastFullWeek":
21116
+ case "lastFullFortnight": {
21117
+ const daysSinceMonday = (weekday + 6) % 7;
21118
+ const daysBack = kind === "lastFullWeek" ? 7 : 14;
21119
+ from = dayStart(-(daysSinceMonday + daysBack));
21120
+ to = dayStart(-daysSinceMonday) - 1;
21121
+ break;
20469
21122
  }
20470
- }
20471
- row.cache = allCached ? { hit: true, age: ages.length > 0 ? Math.max(...ages) : 0 } : { hit: false, age: null };
20472
- const declared = derivedFieldsFor(resource);
20473
- if (declared.length > 0) {
20474
- const byField = new Map(declared.map((d) => [d.field, d]));
20475
- row.derivedFields = declared;
20476
- for (const field of row.fields ?? []) {
20477
- const derivation = byField.get(field.name);
20478
- if (derivation === void 0) continue;
20479
- field.derived = {
20480
- rule: derivation.rule,
20481
- ...derivation.writableRoute !== void 0 ? { writableRoute: derivation.writableRoute } : {}
20482
- };
21123
+ case "lastFullMonth": {
21124
+ const prev = new Date(Date.UTC(y, m - 2, 1));
21125
+ from = localDayStartMs(prev.getUTCFullYear(), prev.getUTCMonth() + 1, 1, zone);
21126
+ to = localDayStartMs(y, m, 1, zone) - 1;
21127
+ break;
20483
21128
  }
20484
- }
20485
- return row;
20486
- }
20487
- async function fieldCatalog(client, opts) {
20488
- validateCatalogOptsShape(opts);
20489
- const { fieldOps, udfCapable, resources: allResources } = capabilitySets();
20490
- const include = resolveInclude(opts);
20491
- const targets = resolveTargets(opts, fieldOps, allResources);
20492
- const fieldsFilter = resolveFieldsFilter(opts);
20493
- const labels = resolveLabels(opts);
20494
- assertWithinWireCap(targets.length, include.length);
20495
- const cache = client.http.fieldCache;
20496
- const zone = client.config.zone ?? "unknown";
20497
- const rows = [];
20498
- const tally = { wireUsed: 0, anyPicklistValues: false };
20499
- const ctx = { client, include, fieldsFilter, labels, fresh: opts?.fresh === true, fieldOps, udfCapable };
20500
- for (const resource of targets) {
20501
- const row = await sweepResource(ctx, resource, tally);
20502
- if (row !== void 0) rows.push(row);
20503
- }
20504
- return {
20505
- zone,
20506
- label: zone,
20507
- cached: cache !== void 0,
20508
- labels,
20509
- ttlMs: cache?.ttlMs ?? null,
20510
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
20511
- rows,
20512
- wireUsed: tally.wireUsed,
20513
- cap: MAX_FIELD_CATALOG_WIRE,
20514
- truncated: false,
20515
- counts: {
20516
- resources: targets.length,
20517
- fieldCapableResources: fieldOps.size,
20518
- udfCapableResources: udfCapable.size,
20519
- cached: rows.filter((r) => r.cache.hit === true).length
20520
- },
20521
- honesty: {
20522
- picklistValues: tally.anyPicklistValues ? "partial" : "unexposed",
20523
- note: FIELD_CATALOG_HONESTY_NOTE
21129
+ case "lastFullQuarter": {
21130
+ const q = Math.floor((m - 1) / 3);
21131
+ const prevQ = q - 1 < 0 ? 3 : q - 1;
21132
+ const prevY = q - 1 < 0 ? y - 1 : y;
21133
+ from = localDayStartMs(prevY, prevQ * 3 + 1, 1, zone);
21134
+ to = localDayStartMs(y, q * 3 + 1, 1, zone) - 1;
21135
+ break;
20524
21136
  }
20525
- };
21137
+ }
21138
+ return { from: iso(from), to: iso(to) };
20526
21139
  }
20527
-
20528
- // src/operations/compositeNames.ts
20529
- 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.)";
20530
- var DEFAULT_MAX_NAME_READS = 20;
20531
- var TICKET_LABEL_FIELDS = ["status", "priority", "ticketType", "issueType", "subIssueType", "source", "queueID"];
20532
- function isVendorId(value) {
20533
- if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0;
20534
- if (typeof value === "string") return /^\d+$/.test(value);
20535
- return false;
21140
+ function dateOnlyWindowBounds(kind, now, tz) {
21141
+ if (tz === void 0 || kind === "last24h") return dateWindowBounds(kind, now, tz);
21142
+ const zone = validateZone(tz);
21143
+ const { from, to } = dateWindowBounds(kind, now, zone);
21144
+ const f = wallParts(zone, Date.parse(from));
21145
+ const t = wallParts(zone, Date.parse(to));
21146
+ return { from: iso(Date.UTC(f.y, f.m - 1, f.d)), to: iso(Date.UTC(t.y, t.m - 1, t.d) + DAY_MS - 1) };
20536
21147
  }
20537
- function createNamePlan(labelFields = []) {
20538
- const resourceIds = [];
20539
- const resSeen = /* @__PURE__ */ new Set();
20540
- const recordFks = [];
20541
- const fkSeen = /* @__PURE__ */ new Set();
20542
- let lineOfBusinessId;
20543
- let lineOfBusinessSource;
20544
- let lineOfBusinessContractId;
20545
- return {
20546
- plan: () => ({
20547
- resourceIds,
20548
- recordFks,
20549
- labelFields,
20550
- ...lineOfBusinessId !== void 0 ? { lineOfBusinessId } : {},
20551
- ...lineOfBusinessSource !== void 0 ? { lineOfBusinessSource } : {},
20552
- ...lineOfBusinessContractId !== void 0 ? { lineOfBusinessContractId } : {}
20553
- }),
20554
- addResource: (value) => {
20555
- if (!isVendorId(value)) return;
20556
- const k = String(value);
20557
- if (!resSeen.has(k)) {
20558
- resSeen.add(k);
20559
- resourceIds.push(value);
20560
- }
20561
- },
20562
- addFk: (table, value) => {
20563
- if (!isVendorId(value)) return;
20564
- const k = `${table}:${String(value)}`;
20565
- if (!fkSeen.has(k)) {
20566
- fkSeen.add(k);
20567
- recordFks.push({ table, id: value });
20568
- }
20569
- },
20570
- addLineOfBusinessFromContract: (value) => {
20571
- if (!isVendorId(value)) return;
20572
- lineOfBusinessContractId = value;
20573
- },
20574
- addLineOfBusiness: (value, origin) => {
20575
- if (!isVendorId(value)) return;
20576
- lineOfBusinessId = value;
20577
- if (origin !== void 0) {
20578
- lineOfBusinessSource = origin.source;
20579
- if (origin.contractId !== void 0) lineOfBusinessContractId = origin.contractId;
20580
- }
20581
- }
20582
- };
21148
+ async function dateWindowQuery(http, path, field, kind, limit, operation, now, tz) {
21149
+ const zone = tz === void 0 ? void 0 : validateZone(tz, operation);
21150
+ const { from, to } = dateOnlyWindowBounds(kind, now, zone);
21151
+ const filter = [
21152
+ { field, op: "gte", value: from },
21153
+ { field, op: "lte", value: to }
21154
+ ];
21155
+ const res = await http.request({
21156
+ method: "POST",
21157
+ idempotent: true,
21158
+ path,
21159
+ body: { MaxRecords: limit, filter },
21160
+ operation
21161
+ });
21162
+ return unwrapItems(res).items;
20583
21163
  }
20584
- function addNameSibling(row, field, resolve) {
20585
- const value = row[field];
20586
- if (value === void 0) return;
20587
- row[`${field}Name`] = isVendorId(value) ? resolve(value) : null;
21164
+ var isoUtc = (ms) => new Date(ms).toISOString().slice(0, 19) + "Z";
21165
+ function customRangeDay(value, label, zone, opName) {
21166
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
21167
+ if (!m) {
21168
+ throw new AutotaskConfigError(
21169
+ zone === void 0 ? `${opName}: ${label} must be a UTC 'YYYY-MM-DD' date (got ${JSON.stringify(value)})` : `${opName}: ${label} must be a 'YYYY-MM-DD' local calendar day in ${zone} (got ${JSON.stringify(value)})`,
21170
+ { operation: opName }
21171
+ );
21172
+ }
21173
+ const y = Number(m[1]);
21174
+ const mo = Number(m[2]);
21175
+ const d = Number(m[3]);
21176
+ const probe = new Date(Date.UTC(y, mo - 1, d));
21177
+ const real = probe.getUTCFullYear() === y && probe.getUTCMonth() === mo - 1 && probe.getUTCDate() === d;
21178
+ const ms = !real ? NaN : zone === void 0 ? Date.UTC(y, mo - 1, d) : localDayStartMs(y, mo, d, zone);
21179
+ if (!Number.isFinite(ms)) {
21180
+ throw new AutotaskConfigError(
21181
+ zone === void 0 ? `${opName}: ${label} is not a valid UTC date (${JSON.stringify(value)})` : `${opName}: ${label} is not a valid calendar day in ${zone} (${JSON.stringify(value)})`,
21182
+ { operation: opName }
21183
+ );
21184
+ }
21185
+ return { y, mo, d, ms };
20588
21186
  }
20589
- function addLabelSibling(row, field, names) {
20590
- const value = row[field];
20591
- if (value === void 0) return;
20592
- row[`${field}Label`] = names.labelFor(field, value);
21187
+ async function customRangeQuery(http, path, field, from, to, limit, operation, zone) {
21188
+ if (zone !== void 0) validateZone(zone, operation);
21189
+ const fromB = customRangeDay(from, "from", void 0, operation);
21190
+ const toB = customRangeDay(to, "to", void 0, operation);
21191
+ if (fromB.ms > toB.ms) {
21192
+ throw new AutotaskConfigError(
21193
+ `${operation}: from (${isoUtc(fromB.ms)}) must not be after to (${isoUtc(toB.ms)})`,
21194
+ { operation }
21195
+ );
21196
+ }
21197
+ const toEndMs = toB.ms + DAY_MS - 1;
21198
+ const filter = [
21199
+ { field, op: "gte", value: isoUtc(fromB.ms) },
21200
+ { field, op: "lte", value: isoUtc(toEndMs) }
21201
+ ];
21202
+ const res = await http.request({
21203
+ method: "POST",
21204
+ idempotent: true,
21205
+ path,
21206
+ body: { MaxRecords: limit, filter },
21207
+ operation
21208
+ });
21209
+ return unwrapItems(res).items;
20593
21210
  }
20594
- var FK_TABLE_PATH = {
20595
- role: "/V1.0/Roles",
20596
- billingCode: "/V1.0/BillingCodes",
20597
- contract: "/V1.0/Contracts"
20598
- };
20599
- var FK_NAME_FIELD = {
20600
- role: "name",
20601
- billingCode: "name",
20602
- contract: "contractName"
20603
- };
20604
- function fkName(record, table) {
20605
- const value = record[FK_NAME_FIELD[table]];
20606
- return typeof value === "string" && value.trim() !== "" ? value : null;
21211
+
21212
+ // src/resources/special/billing_items.ts
21213
+ async function windowQuery(_resource, kind, http, opts) {
21214
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
21215
+ const tz = opts?.tz === void 0 ? void 0 : validateZone(opts.tz, `billing_items.${kind}`);
21216
+ return dateWindowQuery(
21217
+ http,
21218
+ "/V1.0/BillingItems/query",
21219
+ "itemDate",
21220
+ kind,
21221
+ clampWindowLimit(opts?.limit),
21222
+ `billing_items.${kind}`,
21223
+ now,
21224
+ tz
21225
+ );
20607
21226
  }
20608
- function faultRow(err) {
20609
- const e = err;
20610
- return e.httpStatus !== void 0 ? { code: e.code, message: e.message, httpStatus: e.httpStatus } : { code: e.code, message: e.message };
21227
+ async function customRange(_resource, from, to, http, opts) {
21228
+ const zone = opts?.tz === void 0 ? void 0 : validateZone(opts.tz, "billing_items.customRange");
21229
+ return customRangeQuery(
21230
+ http,
21231
+ "/V1.0/BillingItems/query",
21232
+ "itemDate",
21233
+ from,
21234
+ to,
21235
+ clampWindowLimit(opts?.limit),
21236
+ "billing_items.customRange",
21237
+ zone
21238
+ );
20611
21239
  }
20612
- function vendorId(value) {
20613
- if (typeof value === "number") return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
20614
- if (typeof value === "string" && /^[0-9]+$/.test(value.trim())) {
20615
- const n = Number(value.trim());
20616
- return Number.isSafeInteger(n) && n >= 1 ? n : void 0;
21240
+
21241
+ // src/resources/billing_items.ts
21242
+ var BillingItemsResource = class extends BaseResource {
21243
+ constructor(http, pageSize) {
21244
+ super(http, {
21245
+ key: "billing_items",
21246
+ resourcePath: "/V1.0/BillingItems",
21247
+ deletable: false,
21248
+ udfCapable: false,
21249
+ collectionUpdate: true
21250
+ // D1: the spec's item path is GET-only — confirmed updates PUT the collection (id in the body)
21251
+ }, pageSize);
20617
21252
  }
20618
- return void 0;
20619
- }
20620
- async function resolveLineOfBusinessChain(http, associationId, options) {
20621
- const operation = options.operation ?? "compositeNames";
20622
- const degrade = options.onError === "degrade";
20623
- const get = (path) => {
20624
- const res = http.request({ method: "GET", path, operation }).then((body) => {
20625
- const b = body;
20626
- if (b === null || !("item" in b) || b.item === null || b.item === void 0) return null;
20627
- return b.item;
20628
- });
20629
- if (!degrade) {
20630
- return res.catch((e) => {
20631
- if (e instanceof NotFoundError) return null;
20632
- throw e;
20633
- });
20634
- }
20635
- return res.catch((e) => {
20636
- if (e instanceof NotFoundError || isAutotaskError(e) && e.code !== "ABORTED") return null;
20637
- throw e;
20638
- });
20639
- };
20640
- let remaining = options.budget;
20641
- if (remaining < 1) {
20642
- return {
20643
- lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: "capped", organizationalLevel2: "capped", organizationalLevel1: "capped" } },
20644
- used: 0,
20645
- truncated: true
20646
- };
21253
+ /** Query BillingItems records with the G3 filter surface (server-side JSON filter). */
21254
+ query(params) {
21255
+ return this.queryItems(params);
20647
21256
  }
20648
- remaining -= 1;
20649
- const association = await get(`/V1.0/OrganizationalLevelAssociations/${associationId}`);
20650
- const associationMarker = association === null ? "unresolved" : "ok";
20651
- if (association === null) {
20652
- return {
20653
- lineOfBusiness: { association: null, organizationalLevel2: null, organizationalLevel1: null, markers: { association: associationMarker, organizationalLevel2: "missing", organizationalLevel1: "missing" } },
20654
- used: 1,
20655
- truncated: false
20656
- };
21257
+ /** Get every BillingItems record (the MCP-preferred read; 1000-page guarded). */
21258
+ async queryAll(params) {
21259
+ return this.queryAllItems(params);
20657
21260
  }
20658
- const l2Id = vendorId(association.organizationalLevel2ID);
20659
- const l1Id = vendorId(association.organizationalLevel1ID);
20660
- let l2Marker = l2Id !== void 0 ? "unresolved" : "missing";
20661
- let l1Marker = l1Id !== void 0 ? "unresolved" : "missing";
20662
- const need = (l2Id !== void 0 ? 1 : 0) + (l1Id !== void 0 ? 1 : 0);
20663
- let l2 = null;
20664
- let l1 = null;
20665
- let used = 1;
20666
- let truncated = false;
20667
- if (need > 0 && remaining < need) {
20668
- truncated = true;
20669
- if (l2Id !== void 0) l2Marker = "capped";
20670
- if (l1Id !== void 0) l1Marker = "capped";
20671
- } else {
20672
- used += need;
20673
- const [r2, r1] = await Promise.all([
20674
- l2Id !== void 0 ? get(`/V1.0/OrganizationalLevel2s/${l2Id}`) : Promise.resolve(null),
20675
- l1Id !== void 0 ? get(`/V1.0/OrganizationalLevel1s/${l1Id}`) : Promise.resolve(null)
20676
- ]);
20677
- l2 = r2;
20678
- l1 = r1;
20679
- if (l2Id !== void 0) l2Marker = l2 === null ? "unresolved" : "ok";
20680
- if (l1Id !== void 0) l1Marker = l1 === null ? "unresolved" : "ok";
21261
+ /** Count matching records (queryCount envelope unwrapped to a plain number). */
21262
+ async count(params) {
21263
+ return this.countGlobal(params);
20681
21264
  }
20682
- return {
20683
- lineOfBusiness: { association, organizationalLevel2: l2, organizationalLevel1: l1, markers: { association: associationMarker, organizationalLevel2: l2Marker, organizationalLevel1: l1Marker } },
20684
- used,
20685
- truncated
20686
- };
20687
- }
20688
- async function resolveCompositeNames(http, plan, opts = {}) {
20689
- if (opts.maxNameReads !== void 0 && (!Number.isInteger(opts.maxNameReads) || opts.maxNameReads < 1)) {
20690
- throw new AutotaskConfigError(`composite names: maxNameReads must be an integer >= 1 (got ${String(opts.maxNameReads)})`);
21265
+ /** Get one record by id (item envelope; item: null → NotFoundError). `fresh: true` bypasses the read-cache lookup and refreshes the entry (issue #250). */
21266
+ async get(id, opts = {}) {
21267
+ return this.getGlobal(id, opts?.fresh === true);
20691
21268
  }
20692
- const budget = Math.min(DEFAULT_MAX_NAME_READS, opts.maxNameReads ?? DEFAULT_MAX_NAME_READS);
20693
- const client = http.client;
20694
- if (client === void 0) {
20695
- throw new AutotaskConfigError("composite names: the transport is not wired to an AutotaskClient (the composite must run through the client)");
21269
+ /** Update a record (PUT on the COLLECTION — the vendored spec's item path is GET-only; the vendor
21270
+ * keys the record by the id in the body). The vendor validates the FULL model on this route (V2), so a
21271
+ * partial body is merged into the CURRENT record first: one fresh GET (which also serves the
21272
+ * stale-object guard's read) then the collection PUT of the merged model — an explicit null in the
21273
+ * caller body sends null; an omitted field keeps the current value. `raw: true` in the opts bag sends
21274
+ * the body as-is (full model or intentional partial — the compatibility escape hatch; no merge read).
21275
+ * `dryRun: true` returns the pinned envelope whose `data` is the merged body (the exact confirmed
21276
+ * wire) with the best-effort diff, from that same single read; a caller-supplied lastModifiedDate that
21277
+ * mismatches the vendor's current value throws STALE_OBJECT before the mutation. */
21278
+ async update(id, data, opts = {}) {
21279
+ if (opts.dryRun === true) return this.runUpdateDryRun(id, data, opts);
21280
+ return this.updateGlobal(id, data, opts);
20696
21281
  }
20697
- const readCache = http.readCache;
20698
- const nameCache = http.resourceNameCache;
20699
- const section = { truncated: false, rows: {}, reasons: {} };
20700
- let remaining = budget;
20701
- let contractLobPointer;
20702
- let contractLobUnread;
20703
- const coldResource = [];
20704
- for (const id of plan.resourceIds) {
20705
- if (nameCache !== void 0 && nameCache.get(id) !== void 0) continue;
20706
- if (readCache !== void 0 && readCache.get(`/V1.0/Resources/${id}`) !== void 0) continue;
20707
- coldResource.push(id);
21282
+ /** Resolve a BillingItems from a numeric id (or, where the vendor indexes a primary text field,
21283
+ * from a numeric id — this resource has no primary text field). A numeric id miss throws NOT_FOUND;
21284
+ * a bare value rejects with a config error (ids only);
21285
+ * non-match returns null; ambiguity throws RESOLUTION_AMBIGUOUS (candidate ids) and a
21286
+ * cap-tripped bounded client scan throws RESOLUTION_TRUNCATED (never a silent null).
21287
+ * The compact shape is the structural alias of the full record; `expand: true` is the
21288
+ * type-level escape hatch. */
21289
+ async resolve(identifier, opts = {}) {
21290
+ void opts;
21291
+ return this.resolveBase(identifier);
20708
21292
  }
20709
- const withinBudget = coldResource.slice(0, remaining);
20710
- const overBudgetResource = coldResource.slice(remaining);
20711
- remaining -= withinBudget.length;
20712
- for (const id of overBudgetResource) {
20713
- section.rows[`resource:${id}`] = { name: null, blind: false, capped: true };
20714
- section.truncated = true;
21293
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21294
+ customRange(from, to, opts) {
21295
+ return customRange(this, from, to, this.http, opts);
20715
21296
  }
20716
- const overSet = new Set(overBudgetResource.map((id) => String(id)));
20717
- const opIds = plan.resourceIds.filter((id) => !overSet.has(String(id)));
20718
- if (opIds.length > 0) {
20719
- let pass = opIds;
20720
- if (pass.length > 500) {
20721
- for (const id of pass.slice(500)) {
20722
- section.rows[`resource:${id}`] = { name: null, blind: false, capped: true };
20723
- section.truncated = true;
20724
- }
20725
- pass = pass.slice(0, 500);
20726
- }
20727
- const res = await expandResourceNames(client, pass);
20728
- for (const r of res.results) {
20729
- const row = { name: r.name, blind: r.blind, capped: false };
20730
- if (r.error !== void 0) row.error = faultRow(r.error);
20731
- section.rows[`resource:${r.id}`] = row;
20732
- }
21297
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21298
+ last14(opts) {
21299
+ return windowQuery(this, "last14", this.http, opts);
20733
21300
  }
20734
- const holders = {
20735
- role: client.roles,
20736
- billingCode: client.billingCodes,
20737
- contract: client.contracts
20738
- };
20739
- for (const fk of plan.recordFks) {
20740
- const key = `${fk.table}:${fk.id}`;
20741
- const cold = readCache === void 0 || readCache.get(`${FK_TABLE_PATH[fk.table]}/${fk.id}`) === void 0;
20742
- if (cold) {
20743
- if (remaining < 1) {
20744
- section.rows[key] = { name: null, blind: false, capped: true };
20745
- section.truncated = true;
20746
- if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20747
- contractLobUnread = "capped";
20748
- }
20749
- continue;
20750
- }
20751
- remaining -= 1;
20752
- }
20753
- try {
20754
- const record = await holders[fk.table].get(fk.id);
20755
- if (fk.table === "contract" && plan.lineOfBusinessId === void 0 && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20756
- contractLobPointer = vendorId(record["organizationalLevelAssociationID"]);
20757
- }
20758
- section.rows[key] = { name: fkName(record, fk.table), blind: false, capped: false };
20759
- } catch (err) {
20760
- if (err instanceof NotFoundError) {
20761
- section.rows[key] = { name: null, blind: true, capped: false };
20762
- } else if (isAutotaskError(err) && err.code !== "ABORTED") {
20763
- section.rows[key] = { name: null, blind: false, capped: false, error: faultRow(err) };
20764
- if (fk.table === "contract" && plan.lineOfBusinessContractId !== void 0 && String(fk.id) === String(plan.lineOfBusinessContractId)) {
20765
- contractLobUnread = "unresolved";
20766
- }
20767
- } else {
20768
- throw err;
20769
- }
20770
- }
21301
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21302
+ last24h(opts) {
21303
+ return windowQuery(this, "last24h", this.http, opts);
20771
21304
  }
20772
- const picklists = {};
20773
- if (plan.labelFields.length > 0) {
20774
- let usable = true;
20775
- try {
20776
- const catalog = await fieldCatalog(client, { resources: ["tickets"], labels: true, fields: [...plan.labelFields] });
20777
- const row = catalog.rows.find((r) => r.resource === "tickets");
20778
- if (row === void 0 || row.error !== null || row.fields === void 0) {
20779
- usable = false;
20780
- } else {
20781
- for (const f of plan.labelFields) {
20782
- const frow = row.fields.find((x) => x.name === f);
20783
- picklists[f] = frow?.picklistValues;
20784
- }
20785
- }
20786
- } catch (err) {
20787
- if (isAutotaskError(err) && err.code !== "ABORTED") usable = false;
20788
- else throw err;
20789
- }
20790
- if (!usable) for (const f of plan.labelFields) section.reasons[f] = FIELD_CATALOG_HONESTY_NOTE;
21305
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21306
+ last30(opts) {
21307
+ return windowQuery(this, "last30", this.http, opts);
20791
21308
  }
20792
- let lineOfBusiness2 = null;
20793
- const lobId = plan.lineOfBusinessId ?? contractLobPointer;
20794
- if (lobId !== void 0) {
20795
- const chain = await resolveLineOfBusinessChain(http, lobId, { budget: remaining, onError: "degrade" });
20796
- remaining -= chain.used;
20797
- if (chain.truncated) section.truncated = true;
20798
- const fromContract = plan.lineOfBusinessId === void 0;
20799
- lineOfBusiness2 = {
20800
- ...chain.lineOfBusiness,
20801
- ...fromContract ? { source: "contract", contractId: plan.lineOfBusinessContractId } : plan.lineOfBusinessSource !== void 0 ? { source: plan.lineOfBusinessSource } : {},
20802
- ...!fromContract && plan.lineOfBusinessContractId !== void 0 ? { contractId: plan.lineOfBusinessContractId } : {}
20803
- };
20804
- } else if (contractLobUnread !== void 0 && plan.lineOfBusinessContractId !== void 0) {
20805
- lineOfBusiness2 = {
20806
- source: "contract",
20807
- contractId: plan.lineOfBusinessContractId,
20808
- association: null,
20809
- organizationalLevel2: null,
20810
- organizationalLevel1: null,
20811
- markers: { association: contractLobUnread, organizationalLevel2: "missing", organizationalLevel1: "missing" }
20812
- };
21309
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21310
+ last7(opts) {
21311
+ return windowQuery(this, "last7", this.http, opts);
21312
+ }
21313
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21314
+ last90(opts) {
21315
+ return windowQuery(this, "last90", this.http, opts);
21316
+ }
21317
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21318
+ lastFullFortnight(opts) {
21319
+ return windowQuery(this, "lastFullFortnight", this.http, opts);
21320
+ }
21321
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21322
+ lastFullMonth(opts) {
21323
+ return windowQuery(this, "lastFullMonth", this.http, opts);
21324
+ }
21325
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21326
+ lastFullQuarter(opts) {
21327
+ return windowQuery(this, "lastFullQuarter", this.http, opts);
21328
+ }
21329
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21330
+ lastFullWeek(opts) {
21331
+ return windowQuery(this, "lastFullWeek", this.http, opts);
21332
+ }
21333
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21334
+ today(opts) {
21335
+ return windowQuery(this, "today", this.http, opts);
20813
21336
  }
20814
- return {
20815
- section,
20816
- lineOfBusiness: lineOfBusiness2,
20817
- resourceName: (id) => section.rows[`resource:${id}`]?.name ?? null,
20818
- recordName: (table, id) => section.rows[`${table}:${id}`]?.name ?? null,
20819
- labelFor: (field, value) => {
20820
- if (value === null || value === void 0) return null;
20821
- const values = picklists[field];
20822
- if (values === void 0) {
20823
- section.reasons[field] = FIELD_CATALOG_HONESTY_NOTE;
20824
- return null;
20825
- }
20826
- const hit = values.find((v) => String(v.value) === String(value));
20827
- if (hit === void 0) {
20828
- section.reasons[field] = PICKLIST_VALUE_NOT_SERVED_NOTE;
20829
- return null;
20830
- }
20831
- return typeof hit.label === "string" ? hit.label : null;
20832
- }
20833
- };
20834
- }
21337
+ /** Node-parity special op (hand-written in src/resources/special/billing_items.ts). */
21338
+ yesterday(opts) {
21339
+ return windowQuery(this, "yesterday", this.http, opts);
21340
+ }
21341
+ };
20835
21342
 
20836
21343
  // src/resources/special/_shared/dates.ts
20837
21344
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
@@ -20888,22 +21395,6 @@ function num(v) {
20888
21395
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
20889
21396
  }
20890
21397
 
20891
- // src/resources/special/_shared/bounded-part.ts
20892
- async function boundedPart(http, path, conditions, limit, operation, includeFields) {
20893
- const body = { MaxRecords: limit, filter: conditions };
20894
- if (includeFields !== void 0) body.IncludeFields = includeFields;
20895
- const res = await http.request({
20896
- method: "POST",
20897
- idempotent: true,
20898
- path,
20899
- body,
20900
- operation
20901
- });
20902
- const { items, pageDetails } = unwrapItems(res, operation);
20903
- const capped = items.length >= limit;
20904
- return { items, capped, hasMore: capped && pageDetails.nextPageUrl != null };
20905
- }
20906
-
20907
21398
  // src/resources/special/companies/shared.ts
20908
21399
  var DEFAULT_LIMIT2 = 25;
20909
21400
  var MAX_LIMIT2 = 500;
@@ -21252,199 +21743,9 @@ async function upsertByName(resource, companyName, params, opts, http) {
21252
21743
  return createWithDiscovery(resource, body, owner, caveat, opts);
21253
21744
  }
21254
21745
 
21255
- // src/field-sets.ts
21256
- var MODEL_FIELD_SETS = {
21257
- "appointments": ["id", "createDateTime", "creatorResourceID", "description", "endDateTime", "resourceID", "startDateTime", "title", "updateDateTime"],
21258
- "billing_codes": ["id", "afterHoursWorkType", "billingCodeType", "department", "description", "externalNumber", "generalLedgerAccount", "isActive", "isExcludedFromNewContracts", "markupRate", "name", "taxCategoryID", "unitCost", "unitPrice", "useType"],
21259
- "billing_item_approval_levels": ["id", "approvalDateTime", "approvalLevel", "approvalResourceID", "timeEntryID"],
21260
- "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"],
21261
- "change_request_links": ["id", "changeRequestTicketID", "problemOrIncidentTicketID"],
21262
- "checklist_libraries": ["id", "description", "entityType", "isActive", "name"],
21263
- "checklist_library_checklist_items": ["id", "checklistLibraryID", "isImportant", "itemName", "knowledgebaseArticleID", "position", "soapParentPropertyId"],
21264
- "classification_icons": ["id", "description", "isActive", "isSystem", "name", "relativeUrl"],
21265
- "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"],
21266
- "company_alerts": ["id", "alertText", "alertTypeID", "companyID", "soapParentPropertyId"],
21267
- "company_locations": ["id", "address1", "address2", "alternatePhone1", "alternatePhone2", "city", "companyID", "countryID", "description", "fax", "isActive", "isPrimary", "isTaxExempt", "overrideCompanyTaxSettings", "name", "phone", "postalCode", "roundtripDistance", "state", "taxRegionID", "soapParentPropertyId"],
21268
- "company_notes": ["id", "actionType", "assignedResourceID", "companyID", "completedDateTime", "contactID", "createDateTime", "endDateTime", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastModifiedDate", "name", "note", "opportunityID", "startDateTime", "soapParentPropertyId"],
21269
- "company_site_configurations": ["id", "companyID", "locationName", "soapParentPropertyId", "userDefinedFields"],
21270
- "company_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
21271
- "configuration_item_billing_product_associations": ["id", "billingProductID", "configurationItemID", "effectiveDate", "expirationDate", "soapParentPropertyId"],
21272
- "configuration_item_categories": ["id", "displayColorRGB", "isActive", "isClientPortalDefault", "isGlobalDefault", "name", "nickname"],
21273
- "configuration_item_category_udf_associations": ["id", "configurationItemCategoryID", "isRequired", "userDefinedFieldDefinitionID", "soapParentPropertyId"],
21274
- "configuration_item_dns_records": ["id", "createDateTime", "data", "installedProductID", "timeToLiveSeconds", "dnsType", "soapParentPropertyId"],
21275
- "configuration_item_notes": ["id", "configurationItemID", "createDateTime", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "noteType", "title", "soapParentPropertyId"],
21276
- "configuration_item_related_items": ["id", "configurationItemID", "relatedConfigurationItemID", "relationship", "soapParentPropertyId"],
21277
- "configuration_item_ssl_subject_alternative_names": ["id", "configurationItemID", "name", "soapParentPropertyId"],
21278
- "configuration_item_types": ["id", "isActive", "name"],
21279
- "configuration_item_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
21280
- "configuration_items": ["id", "apiVendorID", "configurationItemCategoryID", "companyID", "companyLocationID", "configurationItemType", "contactID", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "createdByPersonID", "dailyCost", "dattoAvailableKilobytes", "dattoDeviceMemoryMegabytes", "dattoDrivesErrors", "dattoHostname", "dattoInternalIP", "dattoKernelVersionID", "dattoLastCheckInDateTime", "dattoNICSpeedKilobitsPerSecond", "dattoNumberOfAgents", "dattoNumberOfDrives", "dattoNumberOfVolumes", "dattoOffsiteUsedBytes", "dattoOSVersionID", "dattoPercentageUsed", "dattoProtectedKilobytes", "dattoRemoteIP", "dattoSerialNumber", "dattoUptimeSeconds", "dattoUsedKilobytes", "dattoZFSVersionID", "deviceNetworkingID", "domain", "domainRegistrarID", "domainRegistrationDateTime", "domainLastUpdatedDateTime", "domainExpirationDateTime", "hourlyCost", "impersonatorCreatorResourceID", "installDate", "installedByContactID", "installedByID", "isActive", "lastActivityPersonID", "lastActivityPersonType", "lastModifiedTime", "location", "monthlyCost", "notes", "numberOfUsers", "parentConfigurationItemID", "perUseCost", "productID", "referenceNumber", "referenceTitle", "rmmDeviceAuditAntivirusStatusID", "rmmDeviceAuditArchitectureID", "rmmDeviceAuditBackupStatusID", "rmmDeviceAuditDescription", "rmmDeviceAuditDeviceTypeID", "rmmDeviceAuditDisplayAdaptorID", "rmmDeviceAuditDomainID", "rmmDeviceAuditExternalIPAddress", "rmmDeviceAuditHostname", "rmmDeviceAuditIPAddress", "rmmDeviceAuditLastUser", "rmmDeviceAuditMacAddress", "rmmDeviceAuditManufacturerID", "rmmDeviceAuditFirmwareID", "rmmDeviceAuditMemoryBytes", "rmmDeviceAuditMissingPatchCount", "rmmDeviceAuditMobileNetworkOperatorID", "rmmDeviceAuditMobileNumber", "rmmDeviceAuditModelID", "rmmDeviceAuditMotherboardID", "rmmDeviceAuditOperatingSystem", "rmmDeviceAuditPatchStatusID", "rmmDeviceAuditProcessorID", "rmmDeviceAuditServicePackID", "rmmDeviceAuditSNMPContact", "rmmDeviceAuditSNMPLocation", "rmmDeviceAuditSNMPName", "rmmDeviceAuditSoftwareStatusID", "rmmDeviceAuditStorageBytes", "rmmDeviceID", "rmmDeviceUID", "rmmVsaxDeviceID", "rmmOpenAlertCount", "serialNumber", "serviceBundleID", "serviceID", "serviceLevelAgreementID", "setupFee", "sourceChargeID", "sourceChargeType", "sslSource", "sslCommonName", "sslValidFromDateTime", "sslValidUntilDateTime", "sslIssuedBy", "sslOrganization", "sslOrganizationUnit", "sslLocation", "sslSerialNumber", "sslSignatureAlgorithm", "vendorID", "warrantyExpirationDate", "rmmIsInMaintenanceMode", "rmmIsMobileDeviceManagementEnrolled", "rmmDeviceUrl", "userDefinedFields"],
21281
- "contact_group_contacts": ["id", "contactId", "contactGroupId", "soapParentPropertyId"],
21282
- "contact_groups": ["id", "name", "isActive"],
21283
- "contact_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
21284
- "contacts": ["id", "additionalAddressInformation", "addressLine", "addressLine1", "alternatePhone", "apiVendorID", "bulkEmailOptOutTime", "city", "companyID", "companyLocationID", "countryID", "createDate", "emailAddress", "emailAddress2", "emailAddress3", "extension", "externalID", "facebookUrl", "faxNumber", "firstName", "impersonatorCreatorResourceID", "isActive", "isOptedOutFromBulkEmail", "lastActivityDate", "lastModifiedDate", "lastName", "linkedInUrl", "middleInitial", "mobilePhone", "namePrefix", "nameSuffix", "note", "receivesEmailNotifications", "phone", "primaryContact", "roomNumber", "solicitationOptOut", "solicitationOptOutTime", "state", "surveyOptOut", "title", "twitterUrl", "zipCode", "soapParentPropertyId", "userDefinedFields"],
21285
- "contract_billing_rules": ["id", "contractID", "createChargesAsBillable", "dailyProratedCost", "dailyProratedPrice", "determineUnits", "endDate", "executionMethod", "includeItemsInChargeDescription", "invoiceDescription", "isActive", "isDailyProrationEnabled", "maximumUnits", "minimumUnits", "productID", "startDate", "soapParentPropertyId"],
21286
- "contract_block_hour_factors": ["id", "blockHourMultiplier", "contractHourlyRate", "contractID", "internalCurrencyContractHourlyRate", "roleID", "soapParentPropertyId"],
21287
- "contract_blocks": ["id", "contractID", "datePurchased", "endDate", "hourlyRate", "hours", "hoursApproved", "invoiceNumber", "isPaid", "paymentNumber", "paymentType", "startDate", "status", "soapParentPropertyId"],
21288
- "contract_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
21289
- "contract_exclusion_billing_codes": ["id", "billingCodeID", "contractID", "soapParentPropertyId"],
21290
- "contract_exclusion_roles": ["id", "contractID", "roleID", "soapParentPropertyId"],
21291
- "contract_exclusion_set_excluded_roles": ["id", "contractExclusionSetID", "excludedRoleID", "soapParentPropertyId"],
21292
- "contract_exclusion_set_excluded_work_types": ["id", "contractExclusionSetID", "excludedWorkTypeID", "soapParentPropertyId"],
21293
- "contract_exclusion_sets": ["id", "description", "isActive", "name"],
21294
- "contract_milestones": ["id", "amount", "billingCodeID", "contractID", "createDate", "creatorResourceID", "dateDue", "description", "internalCurrencyAmount", "isInitialPayment", "organizationalLevelAssociationID", "status", "title", "soapParentPropertyId"],
21295
- "contract_notes": ["id", "contractID", "createDateTime", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "title", "soapParentPropertyId"],
21296
- "contract_rates": ["id", "contractHourlyRate", "contractID", "internalCurrencyContractHourlyRate", "roleID", "soapParentPropertyId"],
21297
- "contract_retainers": ["id", "amount", "amountApproved", "contractID", "datePurchased", "endDate", "internalCurrencyAmount", "internalCurrencyAmountApproved", "invoiceNumber", "isPaid", "paymentID", "paymentNumber", "startDate", "status", "soapParentPropertyId"],
21298
- "contract_role_costs": ["id", "contractID", "rate", "resourceID", "roleID", "soapParentPropertyId"],
21299
- "contract_service_bundle_units": ["id", "approveAndPostDate", "contractID", "contractServiceBundleID", "cost", "endDate", "internalCurrencyPrice", "organizationalLevelAssociationID", "price", "serviceBundleID", "startDate", "units", "soapParentPropertyId"],
21300
- "contract_service_bundles": ["id", "adjustedPrice", "contractID", "internalCurrencyAdjustedPrice", "internalCurrencyUnitPrice", "internalDescription", "invoiceDescription", "quoteItemID", "serviceBundleID", "unitPrice", "soapParentPropertyId"],
21301
- "contract_service_units": ["id", "approveAndPostDate", "contractID", "contractServiceID", "cost", "endDate", "internalCurrencyPrice", "organizationalLevelAssociationID", "price", "serviceID", "startDate", "units", "vendorCompanyID", "soapParentPropertyId"],
21302
- "contract_services": ["id", "contractID", "internalCurrencyAdjustedPrice", "internalCurrencyUnitPrice", "internalDescription", "invoiceDescription", "quoteItemID", "serviceID", "unitCost", "unitPrice", "soapParentPropertyId"],
21303
- "contract_ticket_purchases": ["id", "contractID", "datePurchased", "endDate", "invoiceNumber", "isPaid", "paymentNumber", "paymentType", "perTicketRate", "startDate", "status", "ticketsPurchased", "ticketsUsed", "soapParentPropertyId"],
21304
- "contracts": ["id", "billingPreference", "billToCompanyContactID", "billToCompanyID", "companyID", "contactID", "contactName", "contractCategory", "contractExclusionSetID", "contractName", "contractNumber", "contractPeriodType", "contractType", "description", "endDate", "estimatedCost", "estimatedHours", "estimatedRevenue", "exclusionContractID", "internalCurrencyOverageBillingRate", "internalCurrencySetupFee", "isCompliant", "isDefaultContract", "lastModifiedDateTime", "opportunityID", "organizationalLevelAssociationID", "overageBillingRate", "purchaseOrderNumber", "renewedContractID", "serviceLevelAgreementID", "setupFee", "setupFeeBillingCodeID", "startDate", "status", "timeReportingRequiresStartAndStopTimes", "userDefinedFields"],
21305
- "countries": ["id", "addressFormatID", "countryCode", "displayName", "invoiceTemplateID", "isActive", "isDefaultCountry", "name", "purchaseOrderTemplateID", "quoteTemplateID"],
21306
- "currencies": ["id", "currencyNegativeFormat", "currencyPositiveFormat", "description", "displaySymbol", "exchangeRate", "isActive", "isInternalCurrency", "lastModifiedDateTime", "name", "updateResourceId"],
21307
- "departments": ["id", "description", "name", "number", "primaryLocationID"],
21308
- "domain_registrars": ["id", "name", "url"],
21309
- "expense_item_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "expenseItemID", "expenseReportID", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
21310
- "expense_items": ["id", "companyID", "description", "destination", "entertainmentLocation", "expenseCategory", "expenseCurrencyExpenseAmount", "expenseCurrencyID", "expenseDate", "expenseReportID", "glCode", "haveReceipt", "internalCurrencyExpenseAmount", "internalCurrencyReimbursementAmount", "isBillableToCompany", "isReimbursable", "isRejected", "miles", "odometerEnd", "odometerStart", "origin", "paymentType", "projectID", "purchaseOrderNumber", "reimbursementCurrencyReimbursementAmount", "taskID", "ticketID", "workType", "soapParentPropertyId"],
21311
- "expense_reports": ["id", "amountDue", "approvedDate", "approverID", "departmentNumber", "internalCurrencyCashAdvanceAmount", "internalCurrencyExpenseTotal", "name", "organizationalLevelAssociationID", "quickBooksReferenceNumber", "reimbursementCurrencyAmountDue", "reimbursementCurrencyCashAdvanceAmount", "reimbursementCurrencyID", "rejectionReason", "status", "submit", "submitDate", "submitterID", "weekEnding"],
21312
- "holiday_sets": ["id", "holidaySetDescription", "holidaySetName"],
21313
- "holidays": ["id", "holidayDate", "holidayName", "holidaySetID", "soapParentPropertyId"],
21314
- "inventory_products": ["id", "availableUnits", "backOrderQuantity", "bin", "createDateTime", "createdByResourceID", "inventoryLocationID", "onHandUnits", "pickedUnits", "productID", "quantityMaximum", "quantityMinimum", "referenceNumber", "reservedUnits", "unitsOnOrder"],
21315
- "invoice_templates": ["id", "coveredByBlockRetainerContractLabel", "coveredByRecurringServiceFixedPricePerTicketContractLabel", "currencyNegativeFormat", "currencyPositiveFormat", "dateFormat", "displayFixedPriceContractLabor", "displayRecurringServiceContractLabor", "displaySeparateLineItemForEachTax", "displayTaxCategory", "displayTaxCategorySuperscripts", "displayZeroAmountRecurringServicesAndBundles", "groupBy", "itemizeItemsInEachGroup", "itemizeServicesAndBundles", "name", "nonBillableLaborLabel", "numberFormat", "pageLayout", "pageNumberFormat", "paymentTerms", "rateCostExpression", "showGridHeader", "showVerticalGridLines", "sortBy", "timeFormat"],
21316
- "invoices": ["id", "batchID", "comments", "companyID", "createDateTime", "creatorResourceID", "dueDate", "fromDate", "invoiceDateTime", "invoiceEditorTemplateID", "invoiceNumber", "invoiceTotal", "isVoided", "orderNumber", "paidDate", "paymentTerm", "taxGroup", "taxRegionName", "toDate", "totalTaxValue", "voidedByResourceID", "voidedDate", "webServiceDate"],
21317
- "notification_history": ["id", "companyID", "entityNumber", "entityTitle", "initiatingContactID", "initiatingResourceID", "isActive", "isDeleted", "isTemplateJob", "notificationHistoryTypeID", "notificationSentTime", "opportunityID", "projectID", "quoteID", "recipientDisplayName", "recipientEmailAddress", "taskID", "templateName", "ticketID", "timeEntryID"],
21318
- "opportunities": ["id", "advancedField1", "advancedField2", "advancedField3", "advancedField4", "advancedField5", "amount", "assessmentScore", "barriers", "closedDate", "companyID", "contactID", "cost", "createDate", "creatorResourceID", "description", "helpNeeded", "impersonatorCreatorResourceID", "lastActivity", "leadSource", "lossReason", "lossReasonDetail", "lostDate", "market", "monthlyCost", "monthlyRevenue", "nextStep", "onetimeCost", "onetimeRevenue", "opportunityCategoryID", "organizationalLevelAssociationID", "ownerResourceID", "primaryCompetitor", "probability", "productID", "projectedCloseDate", "promisedFulfillmentDate", "promotionName", "quarterlyCost", "quarterlyRevenue", "rating", "relationshipAssessmentScore", "revenueSpread", "revenueSpreadUnit", "salesOrderID", "salesProcessPercentComplete", "semiannualCost", "semiannualRevenue", "stage", "startDate", "status", "technicalAssessmentScore", "throughDate", "title", "totalAmountMonths", "useQuoteTotals", "winReason", "winReasonDetail", "yearlyCost", "yearlyRevenue", "userDefinedFields"],
21319
- "opportunity_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentAttachmentID", "parentID", "publish", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
21320
- "opportunity_categories": ["id", "displayColorRGB", "isActive", "isGlobalDefault", "name", "nickname"],
21321
- "organizational_level1s": ["id", "description", "isActive", "name"],
21322
- "organizational_level2s": ["id", "description", "isActive", "name"],
21323
- "organizational_level_associations": ["id", "isActive", "organizationalLevel1ID", "organizationalLevel2ID"],
21324
- "phases": ["id", "createDate", "creatorResourceID", "description", "dueDate", "estimatedHours", "externalID", "isScheduled", "lastActivityDateTime", "parentPhaseID", "phaseNumber", "projectID", "startDate", "title", "soapParentPropertyId"],
21325
- "product_vendors": ["id", "isActive", "isDefault", "productID", "vendorCost", "vendorID", "vendorPartNumber", "soapParentPropertyId"],
21326
- "products": ["id", "billingType", "chargeBillingCodeID", "createdByResourceID", "createdTime", "defaultVendorID", "description", "doesNotRequireProcurement", "externalProductID", "impersonatorCreatorResourceID", "internalProductID", "isActive", "isEligibleForRma", "isSerialized", "link", "manufacturerName", "manufacturerProductName", "markupRate", "msrp", "name", "periodType", "priceCostMethod", "productBillingCodeID", "productCategory", "sku", "unitCost", "unitPrice", "vendorProductNumber", "defaultInstalledProductCategoryID", "userDefinedFields"],
21327
- "project_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "estimatedCost", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "projectID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
21328
- "project_notes": ["id", "createDateTime", "createdByContactID", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "isAnnouncement", "lastActivityDate", "noteType", "projectID", "publish", "title", "soapParentPropertyId"],
21329
- "projects": ["id", "actualBilledHours", "actualHours", "changeOrdersBudget", "changeOrdersRevenue", "companyID", "companyOwnerResourceID", "completedDateTime", "completedPercentage", "contractID", "createDateTime", "creatorResourceID", "department", "description", "duration", "endDateTime", "estimatedSalesCost", "estimatedTime", "extProjectNumber", "extProjectType", "impersonatorCreatorResourceID", "laborEstimatedCosts", "laborEstimatedMarginPercentage", "laborEstimatedRevenue", "lastActivityDateTime", "lastActivityPersonType", "lastActivityResourceID", "opportunityID", "organizationalLevelAssociationID", "originalEstimatedRevenue", "projectCostEstimatedMarginPercentage", "projectCostsBudget", "projectCostsRevenue", "projectLeadResourceID", "projectName", "projectNumber", "projectType", "purchaseOrderNumber", "sgda", "startDateTime", "status", "statusDateTime", "statusDetail", "userDefinedFields"],
21330
- "purchase_approvals": ["id", "costType", "isApproved", "rejectNote"],
21331
- "purchase_order_item_receiving": ["id", "purchaseOrderItemID", "quantityBackOrdered", "quantityNowReceiving", "quantityPreviouslyReceived", "receiveDate", "receivedByResourceID", "serialNumber", "vendorInvoiceNumber", "soapParentPropertyId"],
21332
- "purchase_order_items": ["id", "chargeID", "contractID", "estimatedArrivalDate", "internalCurrencyUnitCost", "inventoryLocationID", "memo", "orderID", "productID", "projectID", "quantity", "salesOrderID", "ticketID", "unitCost", "soapParentPropertyId"],
21333
- "purchase_orders": ["id", "cancelDateTime", "createDateTime", "creatorResourceID", "externalPONumber", "fax", "freight", "generalMemo", "impersonatorCreatorResourceID", "internalCurrencyFreight", "latestEstimatedArrivalDate", "paymentTerm", "phone", "purchaseForCompanyID", "purchaseOrderNumber", "purchaseOrderTemplateID", "shippingDate", "shippingType", "shipToAddress1", "shipToAddress2", "shipToCity", "shipToName", "shipToPostalCode", "shipToState", "showEachTaxInGroup", "showTaxCategory", "status", "submitDateTime", "taxRegionID", "useItemDescriptionsFrom", "vendorID", "vendorInvoiceNumber", "additionalVendorInvoiceNumbers"],
21334
- "quote_items": ["id", "averageCost", "chargeID", "description", "expenseID", "highestCost", "internalCurrencyLineDiscount", "internalCurrencyUnitDiscount", "internalCurrencyUnitPrice", "isOptional", "isTaxable", "laborID", "lineDiscount", "markupRate", "name", "percentageDiscount", "periodType", "productID", "quantity", "quoteID", "quoteItemType", "serviceBundleID", "serviceID", "shippingID", "sortOrderID", "taxCategoryID", "totalEffectiveTax", "unitCost", "unitDiscount", "unitPrice", "soapParentPropertyId"],
21335
- "quote_locations": ["id", "address1", "address2", "city", "postalCode", "state"],
21336
- "quote_templates": ["id", "calculateTaxSeparately", "createDate", "createdBy", "currencyNegativeFormat", "currencyPositiveFormat", "dateFormat", "description", "displayTaxCategorySuperscripts", "isActive", "lastActivityBy", "lastActivityDate", "name", "numberFormat", "pageLayout", "pageNumberFormat", "showEachTaxInGroup", "showGridHeader", "showTaxCategory", "showVerticalGridLines"],
21337
- "quotes": ["id", "approvalStatus", "approvalStatusChangedByResourceID", "approvalStatusChangedDate", "billToLocationID", "calculateTaxSeparately", "comment", "companyID", "contactID", "createDate", "creatorResourceID", "description", "effectiveDate", "expirationDate", "extApprovalContactResponse", "extApprovalResponseDate", "extApprovalResponseSignature", "externalQuoteNumber", "groupByID", "impersonatorCreatorResourceID", "isActive", "lastActivityDate", "lastModifiedBy", "lastPublishedByResourceID", "lastPublishedDateTime", "name", "opportunityID", "paymentTerm", "paymentType", "primaryQuote", "proposalProjectID", "purchaseOrderNumber", "quoteNumber", "quoteTemplateID", "shippingType", "shipToLocationID", "showEachTaxInGroup", "showTaxCategory", "soldToLocationID", "taxRegionID"],
21338
- "resource_role_queues": ["id", "queueID", "resourceID", "soapParentPropertyId"],
21339
- "resource_roles": ["id", "departmentID", "isActive", "queueID", "resourceID", "roleID", "soapParentPropertyId"],
21340
- "resource_skills": ["id", "resourceID", "skillDescription", "skillID", "skillLevel", "soapParentPropertyId"],
21341
- "resource_time_off_approvers": ["id", "resourceID", "approverResourceID", "approvalLevel", "soapParentPropertyId"],
21342
- "resource_time_off_balances": ["id", "resourceID", "floatingHolidayAnnualAllowance", "floatingHolidayBalance", "floatingHolidayPlanned", "floatingHolidayUsed", "floatingHolidayWaitingApproval", "personalTimeAnnualAllowance", "personalTimeBalance", "personalTimePlanned", "personalTimeUsed", "personalTimeWaitingApproval", "sickTimeAnnualAllowance", "sickTimeBalance", "sickTimePlanned", "sickTimeUsed", "sickTimeWaitingApproval", "vacationTimeAnnualAllowance", "vacationTimeBalance", "vacationTimePlanned", "vacationTimeUsed", "vacationTimeWaitingApproval", "year", "soapParentPropertyId"],
21343
- "resources": ["id", "accountingReferenceID", "dateFormat", "defaultServiceDeskRoleID", "email", "email2", "email3", "emailTypeCode", "emailTypeCode2", "emailTypeCode3", "firstName", "gender", "greeting", "hireDate", "homePhone", "initials", "internalCost", "isActive", "lastName", "licenseType", "locationID", "middleName", "mobilePhone", "numberFormat", "officeExtension", "officePhone", "payrollIdentifier", "payrollType", "resourceType", "suffix", "surveyResourceRating", "timeFormat", "title", "travelAvailabilityPct", "userName", "userType"],
21344
- "roles": ["id", "description", "hourlyFactor", "hourlyRate", "isActive", "isExcludedFromNewContracts", "isSystemRole", "name", "quoteItemDefaultTaxCategoryId", "roleType"],
21345
- "sales_orders": ["id", "additionalBillToAddressInformation", "additionalShipToAddressInformation", "billingAddress1", "billingAddress2", "billToCity", "billToCountryID", "billToPostalCode", "billToState", "companyID", "contactID", "impersonatorCreatorResourceID", "opportunityID", "organizationalLevelAssociationID", "ownerResourceID", "promisedFulfillmentDate", "salesOrderDate", "shipToAddress1", "shipToAddress2", "shipToCity", "shipToCountryID", "shipToPostalCode", "shipToState", "status", "title", "soapParentPropertyId", "userDefinedFields"],
21346
- "service_call_task_resources": ["id", "resourceID", "serviceCallTaskID", "soapParentPropertyId"],
21347
- "service_call_tasks": ["id", "serviceCallID", "taskID", "soapParentPropertyId"],
21348
- "service_call_ticket_resources": ["id", "resourceID", "serviceCallTicketID", "soapParentPropertyId"],
21349
- "service_call_tickets": ["id", "serviceCallID", "ticketID", "soapParentPropertyId"],
21350
- "service_calls": ["id", "cancelationNoticeHours", "canceledByResourceID", "canceledDateTime", "companyID", "companyLocationID", "createDateTime", "creatorResourceID", "description", "duration", "endDateTime", "impersonatorCreatorResourceID", "isComplete", "lastModifiedDateTime", "startDateTime", "status"],
21351
- "service_level_agreement_results": ["id", "firstResponseElapsedHours", "firstResponseInitiatingResourceID", "firstResponseResourceID", "isFirstResponseMet", "isResolutionMet", "isResolutionPlanMet", "resolutionElapsedHours", "resolutionPlanElapsedHours", "resolutionPlanResourceID", "resolutionResourceID", "serviceLevelAgreementName", "ticketID", "soapParentPropertyId"],
21352
- "services": ["id", "billingCodeID", "createDate", "creatorResourceID", "description", "invoiceDescription", "isActive", "lastModifiedDate", "markupRate", "name", "periodType", "serviceLevelAgreementID", "unitCost", "unitPrice", "updateResourceID", "vendorCompanyID", "manufacturerServiceProvider", "manufacturerServiceProviderProductNumber", "catalogNumberPartNumber", "sku", "internalID", "externalID", "url", "userDefinedFields"],
21353
- "skills": ["id", "categoryID", "description", "isActive", "name"],
21354
- "subscription_periods": ["id", "periodCost", "periodDate", "periodPrice", "postedDate", "purchaseOrderNumber", "subscriptionID", "soapParentPropertyId"],
21355
- "subscriptions": ["id", "configurationItemID", "description", "effectiveDate", "expirationDate", "impersonatorCreatorResourceID", "materialCodeID", "organizationalLevelAssociationID", "periodCost", "periodPrice", "periodType", "purchaseOrderNumber", "status", "subscriptionName", "totalCost", "totalPrice", "vendorID", "userDefinedFields"],
21356
- "survey_results": ["id", "companyID", "companyRating", "completeDate", "contactID", "contactRating", "resourceRating", "sendDate", "surveyID", "surveyRating", "ticketID"],
21357
- "surveys": ["id", "description", "displayName", "name"],
21358
- "tag_aliases": ["id", "alias", "tagID", "soapParentPropertyId"],
21359
- "tag_groups": ["id", "displayColor", "isActive", "isSystem", "label"],
21360
- "tags": ["id", "createDateTime", "isActive", "isExcludedFromAutomaticTagging", "isSystem", "label", "lastModifiedDateTime", "tagGroupID"],
21361
- "task_secondary_resources": ["id", "resourceID", "roleID", "taskID", "soapParentPropertyId"],
21362
- "tasks": ["id", "assignedResourceID", "assignedResourceRoleID", "billingCodeID", "canClientPortalUserCompleteTask", "companyLocationID", "completedByResourceID", "completedByType", "completedDateTime", "createDateTime", "creatorResourceID", "creatorType", "departmentID", "description", "endDateTime", "estimatedHours", "externalID", "hoursToBeScheduled", "isTaskBillable", "isVisibleInClientPortal", "lastActivityDateTime", "lastActivityPersonType", "lastActivityResourceID", "phaseID", "priority", "priorityLabel", "projectID", "purchaseOrderNumber", "remainingHours", "startDateTime", "status", "taskCategoryID", "taskNumber", "taskType", "title", "soapParentPropertyId", "userDefinedFields"],
21363
- "tax_regions": ["id", "isActive", "name"],
21364
- "ticket_additional_configuration_items": ["id", "configurationItemID", "ticketID", "soapParentPropertyId"],
21365
- "ticket_additional_contacts": ["id", "contactID", "ticketID", "soapParentPropertyId"],
21366
- "ticket_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentAttachmentID", "parentID", "publish", "ticketID", "ticketNoteID", "timeEntryID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
21367
- "ticket_categories": ["id", "displayColorRGB", "isActive", "isApiOnly", "isGlobalDefault", "name", "nickname"],
21368
- "ticket_category_field_defaults": ["id", "description", "estimatedHours", "issueTypeID", "organizationalLevelAssociationID", "priority", "purchaseOrderNumber", "queueID", "resolution", "serviceLevelAgreementID", "sourceID", "status", "subIssueTypeID", "ticketCategoryID", "ticketTypeID", "title", "workTypeID", "soapParentPropertyId"],
21369
- "ticket_change_request_approvals": ["id", "approveRejectDateTime", "approveRejectNote", "contactID", "isApproved", "resourceID", "ticketID", "soapParentPropertyId"],
21370
- "ticket_charges": ["id", "billableAmount", "billingCodeID", "chargeType", "contractServiceBundleID", "contractServiceID", "createDate", "creatorResourceID", "datePurchased", "description", "extendedCost", "internalCurrencyBillableAmount", "internalCurrencyUnitPrice", "internalPurchaseOrderNumber", "isBillableToCompany", "isBilled", "name", "notes", "organizationalLevelAssociationID", "productID", "purchaseOrderNumber", "status", "statusLastModifiedBy", "statusLastModifiedDate", "ticketID", "unitCost", "unitPrice", "unitQuantity", "soapParentPropertyId"],
21371
- "ticket_checklist_items": ["id", "completedByResourceID", "completedDateTime", "isCompleted", "isImportant", "itemName", "knowledgebaseArticleID", "position", "ticketID", "soapParentPropertyId"],
21372
- "ticket_history": ["id", "action", "date", "detail", "resourceID", "ticketID", "soapParentPropertyId"],
21373
- "ticket_note_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "ticketID", "ticketNoteID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
21374
- "ticket_note_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
21375
- "ticket_notes": ["id", "createDateTime", "createdByContactID", "creatorResourceID", "description", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "lastActivityDate", "noteType", "publish", "ticketID", "title", "soapParentPropertyId"],
21376
- "ticket_secondary_resources": ["id", "resourceID", "roleID", "ticketID", "soapParentPropertyId"],
21377
- "ticket_webhooks": ["id", "deactivationUrl", "isActive", "isReady", "isSubscribedToCreateEvents", "isSubscribedToDeleteEvents", "isSubscribedToUpdateEvents", "name", "notificationEmailAddress", "ownerResourceID", "secretKey", "sendThresholdExceededNotification", "webhookGUID", "webhookUrl"],
21378
- "tickets": ["id", "apiVendorID", "assignedResourceID", "assignedResourceRoleID", "billingCodeID", "changeApprovalBoard", "changeApprovalStatus", "changeApprovalType", "changeInfoField1", "changeInfoField2", "changeInfoField3", "changeInfoField4", "changeInfoField5", "companyID", "companyLocationID", "completedByResourceID", "completedDate", "configurationItemID", "contactID", "contractID", "contractServiceBundleID", "contractServiceID", "createDate", "createdByContactID", "creatorResourceID", "creatorType", "currentServiceThermometerRating", "description", "dueDateTime", "estimatedHours", "externalID", "firstResponseAssignedResourceID", "firstResponseDateTime", "firstResponseDueDateTime", "firstResponseInitiatingResourceID", "hoursToBeScheduled", "impersonatorCreatorResourceID", "isAssignedToComanaged", "issueType", "isVisibleToComanaged", "lastActivityDate", "lastActivityPersonType", "lastActivityResourceID", "lastCustomerNotificationDateTime", "lastCustomerVisibleActivityDateTime", "lastTrackedModificationDateTime", "monitorID", "monitorTypeID", "opportunityID", "organizationalLevelAssociationID", "previousServiceThermometerRating", "priority", "problemTicketId", "projectID", "purchaseOrderNumber", "queueID", "resolution", "resolutionPlanDateTime", "resolutionPlanDueDateTime", "resolvedDateTime", "resolvedDueDateTime", "rmaStatus", "rmaType", "rmmAlertID", "serviceLevelAgreementHasBeenMet", "serviceLevelAgreementID", "serviceLevelAgreementPausedNextEventHours", "serviceThermometerTemperature", "source", "status", "subIssueType", "ticketCategory", "ticketNumber", "ticketType", "title", "userDefinedFields"],
21379
- "time_entries": ["id", "billingApprovalDateTime", "billingApprovalLevelMostRecent", "billingApprovalResourceID", "billingCodeID", "contractID", "contractServiceBundleID", "contractServiceID", "createDateTime", "creatorUserID", "dateWorked", "endDateTime", "hoursToBill", "hoursWorked", "impersonatorCreatorResourceID", "impersonatorUpdaterResourceID", "internalBillingCodeID", "internalNotes", "isInternalNotesVisibleToComanaged", "isNonBillable", "lastModifiedDateTime", "lastModifiedUserID", "offsetHours", "resourceID", "roleID", "showOnInvoice", "startDateTime", "summaryNotes", "taskID", "ticketID", "timeEntryType"],
21380
- "time_entry_attachments": ["id", "attachDate", "attachedByContactID", "attachedByResourceID", "attachmentType", "contentType", "creatorType", "fileSize", "fullPath", "impersonatorCreatorResourceID", "opportunityID", "parentID", "publish", "taskID", "ticketID", "timeEntryID", "title", "data", "parentType", "soapParentPropertyId", "isTaskAttachment"],
21381
- "time_off_requests": ["id", "approvedDateTime", "approveRejectResourceID", "createdByResourceID", "createDateTime", "endTime", "hours", "impersonatorApproveRejectResourceID", "lastModifiedByResourceID", "lastModifiedDateTime", "lastApprovedLevel", "reason", "rejectReason", "requestDate", "resourceID", "startTime", "status", "timeOffRequestType", "soapParentPropertyId"]
21382
- };
21383
- var MODEL_DATE_FIELDS = {
21384
- "appointments": ["createDateTime", "endDateTime", "startDateTime", "updateDateTime"],
21385
- "billing_item_approval_levels": ["approvalDateTime"],
21386
- "billing_items": ["itemDate", "postedDate", "postedOnTime", "webServiceDate"],
21387
- "companies": ["createDate", "lastActivityDate", "lastTrackedModifiedDateTime"],
21388
- "company_notes": ["completedDateTime", "createDateTime", "endDateTime", "lastModifiedDate", "startDateTime"],
21389
- "configuration_item_billing_product_associations": ["effectiveDate", "expirationDate"],
21390
- "configuration_item_dns_records": ["createDateTime"],
21391
- "configuration_item_notes": ["createDateTime", "lastActivityDate"],
21392
- "configuration_items": ["createDate", "dattoLastCheckInDateTime", "domainRegistrationDateTime", "domainLastUpdatedDateTime", "domainExpirationDateTime", "installDate", "lastModifiedTime", "sslValidFromDateTime", "sslValidUntilDateTime", "warrantyExpirationDate"],
21393
- "contacts": ["bulkEmailOptOutTime", "createDate", "lastActivityDate", "lastModifiedDate", "solicitationOptOutTime"],
21394
- "contract_billing_rules": ["endDate", "startDate"],
21395
- "contract_blocks": ["datePurchased", "endDate", "startDate"],
21396
- "contract_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
21397
- "contract_milestones": ["createDate", "dateDue"],
21398
- "contract_notes": ["createDateTime", "lastActivityDate"],
21399
- "contract_retainers": ["datePurchased", "endDate", "startDate"],
21400
- "contract_service_bundle_units": ["approveAndPostDate", "endDate", "startDate"],
21401
- "contract_service_units": ["approveAndPostDate", "endDate", "startDate"],
21402
- "contract_ticket_purchases": ["datePurchased", "endDate", "startDate"],
21403
- "contracts": ["endDate", "lastModifiedDateTime", "startDate"],
21404
- "currencies": ["lastModifiedDateTime"],
21405
- "expense_item_attachments": ["attachDate"],
21406
- "expense_items": ["expenseDate"],
21407
- "expense_reports": ["approvedDate", "submitDate", "weekEnding"],
21408
- "holidays": ["holidayDate"],
21409
- "inventory_products": ["createDateTime"],
21410
- "invoices": ["createDateTime", "dueDate", "fromDate", "invoiceDateTime", "paidDate", "toDate", "voidedDate", "webServiceDate"],
21411
- "notification_history": ["notificationSentTime"],
21412
- "opportunities": ["closedDate", "createDate", "lastActivity", "lostDate", "projectedCloseDate", "promisedFulfillmentDate", "startDate", "throughDate"],
21413
- "opportunity_attachments": ["attachDate"],
21414
- "phases": ["createDate", "dueDate", "lastActivityDateTime", "startDate"],
21415
- "products": ["createdTime"],
21416
- "project_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
21417
- "project_notes": ["createDateTime", "lastActivityDate"],
21418
- "projects": ["completedDateTime", "createDateTime", "endDateTime", "lastActivityDateTime", "startDateTime", "statusDateTime"],
21419
- "purchase_order_item_receiving": ["receiveDate"],
21420
- "purchase_order_items": ["estimatedArrivalDate"],
21421
- "purchase_orders": ["cancelDateTime", "createDateTime", "latestEstimatedArrivalDate", "shippingDate", "submitDateTime"],
21422
- "quote_templates": ["createDate", "lastActivityDate"],
21423
- "quotes": ["approvalStatusChangedDate", "createDate", "effectiveDate", "expirationDate", "extApprovalResponseDate", "lastActivityDate", "lastPublishedDateTime"],
21424
- "resources": ["hireDate"],
21425
- "sales_orders": ["promisedFulfillmentDate", "salesOrderDate"],
21426
- "service_calls": ["canceledDateTime", "createDateTime", "endDateTime", "lastModifiedDateTime", "startDateTime"],
21427
- "services": ["createDate", "lastModifiedDate"],
21428
- "subscription_periods": ["periodDate", "postedDate"],
21429
- "subscriptions": ["effectiveDate", "expirationDate"],
21430
- "survey_results": ["completeDate", "sendDate"],
21431
- "tags": ["createDateTime", "lastModifiedDateTime"],
21432
- "tasks": ["completedDateTime", "createDateTime", "endDateTime", "lastActivityDateTime", "startDateTime"],
21433
- "ticket_attachments": ["attachDate"],
21434
- "ticket_change_request_approvals": ["approveRejectDateTime"],
21435
- "ticket_charges": ["createDate", "datePurchased", "statusLastModifiedDate"],
21436
- "ticket_checklist_items": ["completedDateTime"],
21437
- "ticket_history": ["date"],
21438
- "ticket_note_attachments": ["attachDate"],
21439
- "ticket_notes": ["createDateTime", "lastActivityDate"],
21440
- "tickets": ["completedDate", "createDate", "dueDateTime", "firstResponseDateTime", "firstResponseDueDateTime", "lastActivityDate", "lastCustomerNotificationDateTime", "lastCustomerVisibleActivityDateTime", "lastTrackedModificationDateTime", "resolutionPlanDateTime", "resolutionPlanDueDateTime", "resolvedDateTime", "resolvedDueDateTime"],
21441
- "time_entries": ["billingApprovalDateTime", "createDateTime", "dateWorked", "endDateTime", "lastModifiedDateTime", "startDateTime"],
21442
- "time_entry_attachments": ["attachDate"],
21443
- "time_off_requests": ["approvedDateTime", "createDateTime", "endTime", "lastModifiedDateTime", "requestDate", "startTime"]
21444
- };
21445
-
21446
21746
  // src/operations/invoke/fieldNames.ts
21447
21747
  async function liveFieldNames(holder) {
21748
+ if (holder === void 0) return null;
21448
21749
  const reader = holder.getFieldInfo;
21449
21750
  if (typeof reader !== "function") return null;
21450
21751
  try {
@@ -21478,7 +21779,7 @@ function collectFilterFieldNames(conditions) {
21478
21779
  continue;
21479
21780
  }
21480
21781
  if (cond.udf === true) continue;
21481
- if (typeof cond.field === "string" && cond.field.length > 0) out.push(cond.field);
21782
+ if (typeof cond.field === "string" && cond.field.length > 0 && !cond.field.includes(".")) out.push(cond.field);
21482
21783
  }
21483
21784
  };
21484
21785
  walk(conditions);
@@ -21556,16 +21857,19 @@ async function validateQueryFieldNames(record, holder, args) {
21556
21857
  filterNames = collectFilterFieldNames(conditions ?? []);
21557
21858
  }
21558
21859
  if (fieldCheck === false) return;
21559
- const hasInclude = includeNames !== void 0 && includeNames.length > 0;
21560
- const hasFilter = filterNames !== void 0 && filterNames.length > 0;
21561
- if (!hasInclude && !hasFilter) return;
21860
+ const found = await findUnknownFieldNames(record, holder, { includeFields: includeNames ?? [], filter: filterNames ?? [] });
21861
+ if (found === void 0) return;
21862
+ const { known, authority, unknown } = found;
21863
+ if (unknown["includeFields"].length === 0 && unknown["filter"].length === 0) return;
21864
+ throw unknownNamesRefusal(record, authority, known, unknown["includeFields"], unknown["filter"]);
21865
+ }
21866
+ async function findUnknownFieldNames(record, holder, lists) {
21867
+ if (Object.values(lists).every((names) => names.length === 0)) return void 0;
21562
21868
  const resolved = resolveAuthority(record, await liveFieldNames(holder));
21563
- if (resolved === void 0) return;
21564
- const { known, authority } = resolved;
21565
- const unknownInclude = hasInclude ? includeNames.filter((name) => !known.includes(name)) : [];
21566
- const unknownFilter = hasFilter ? [...new Set(filterNames.filter((name) => !known.includes(name)))] : [];
21567
- if (unknownInclude.length === 0 && unknownFilter.length === 0) return;
21568
- throw unknownNamesRefusal(record, authority, known, unknownInclude, unknownFilter);
21869
+ if (resolved === void 0) return void 0;
21870
+ const unknown = {};
21871
+ for (const [key, names] of Object.entries(lists)) unknown[key] = [...new Set(names.filter((name) => !resolved.known.includes(name)))];
21872
+ return { ...resolved, unknown };
21569
21873
  }
21570
21874
 
21571
21875
  // src/resources/special/_shared/ticketStatus.ts
@@ -21995,7 +22299,7 @@ async function prepareCrmWrite(resource, input, http) {
21995
22299
 
21996
22300
  // src/resources/special/companies/unbilledWorkTypes.ts
21997
22301
  var UNBILLED_WORK_NOTE_CHARS_DEFAULT = 400;
21998
- var UNBILLED_WORK_JOIN_SET_CAP = 300;
22302
+ var UNBILLED_WORK_JOIN_SET_CAP = JOIN_SET_CAP;
21999
22303
 
22000
22304
  // src/resources/special/companies/unbilledWorkValidate.ts
22001
22305
  var UNBILLED_WORK_OPT_KEYS = [
@@ -22104,11 +22408,6 @@ function validateUnbilledWorkOptions(companyId, opts) {
22104
22408
 
22105
22409
  // src/resources/special/companies/unbilledWork.ts
22106
22410
  var OP3 = "companies.unbilledWork";
22107
- function capJoinSet(ids2, cap) {
22108
- const sorted = [...new Set(ids2)].sort((a, b) => a - b);
22109
- if (sorted.length <= cap) return { ids: sorted, truncated: false };
22110
- return { ids: sorted.slice(0, cap), truncated: true };
22111
- }
22112
22411
  function unbilledWorkPartStatus(key, conditions, items) {
22113
22412
  if (items.length !== 0) return { status: "ok" };
22114
22413
  const stamp = blindStampFor(BLIND_SURFACE_STAMPS[key]?.query, conditions);
@@ -22554,6 +22853,7 @@ async function fetchMany(client, items, opts = {}) {
22554
22853
  if (opts.liveFields !== void 0 && typeof opts.liveFields !== "boolean") {
22555
22854
  throw new AutotaskConfigError(`fetchMany: liveFields must be a boolean (got ${typeof opts.liveFields})`);
22556
22855
  }
22856
+ const labels = readNamesOpt(opts.names, "fetchMany");
22557
22857
  const cap = Math.min(FETCH_MANY_MAX_ITEMS, opts.maxItems ?? FETCH_MANY_MAX_ITEMS);
22558
22858
  if (opts.liveFields === true) {
22559
22859
  validateFetchManyItems(items, cap, void 0, void 0, { fieldCheck: false });
@@ -22609,7 +22909,8 @@ async function fetchMany(client, items, opts = {}) {
22609
22909
  }
22610
22910
  }
22611
22911
  const ok = results.filter((r) => r.found).length;
22612
- return { results, ok, failed: results.length - ok };
22912
+ const out = { results, ok, failed: results.length - ok };
22913
+ return labels ? labelFetchResults(client, out) : out;
22613
22914
  }
22614
22915
 
22615
22916
  // src/operations/fetchByIds.ts
@@ -22637,6 +22938,7 @@ async function fetchByIds(client, items, opts = {}) {
22637
22938
  throw new AutotaskConfigError(`fetchByIds: maxItems must be an integer >= 1 (got ${String(opts.maxItems)})`);
22638
22939
  }
22639
22940
  }
22941
+ const labels = readNamesOpt(opts.names, "fetchByIds");
22640
22942
  const cap = Math.min(FETCH_BY_IDS_MAX_ITEMS, opts.maxItems ?? FETCH_BY_IDS_MAX_ITEMS);
22641
22943
  validateFetchManyItems(items, cap, "fetchByIds", `FETCH_BY_IDS_MAX_ITEMS = ${FETCH_BY_IDS_MAX_ITEMS}`);
22642
22944
  const groups = /* @__PURE__ */ new Map();
@@ -22682,8 +22984,8 @@ async function fetchByIds(client, items, opts = {}) {
22682
22984
  maxRecords: FETCH_BY_IDS_CHUNK_SIZE,
22683
22985
  includeFields
22684
22986
  });
22685
- const res = await client.http.request({ ...req, operation: `${key}.fetchByIds` });
22686
- const { items: rows } = unwrapItems(res, `${key}.fetchByIds`);
22987
+ const res2 = await client.http.request({ ...req, operation: `${key}.fetchByIds` });
22988
+ const { items: rows } = unwrapItems(res2, `${key}.fetchByIds`);
22687
22989
  for (const row of rows) {
22688
22990
  const k = String(row.id);
22689
22991
  if (!chunkKeys.has(k)) continue;
@@ -22733,7 +23035,8 @@ async function fetchByIds(client, items, opts = {}) {
22733
23035
  );
22734
23036
  }
22735
23037
  const ok = results.filter((r) => r.found).length;
22736
- return { results, ok, failed: results.length - ok };
23038
+ const res = { results, ok, failed: results.length - ok };
23039
+ return labels ? labelFetchResults(client, res) : res;
22737
23040
  }
22738
23041
 
22739
23042
  // src/resources/special/contracts/lineOfBusiness.ts
@@ -26048,7 +26351,8 @@ var TimeEntriesResource = class extends BaseResource {
26048
26351
  postedWithin(from, to, opts) {
26049
26352
  return postedWithin(this, from, to, this.http, opts);
26050
26353
  }
26051
- /** Node-parity special op (hand-written in src/resources/special/time_entries.ts). */
26354
+ /** Node-parity special op (hand-written in src/resources/special/time_entries.ts).
26355
+ * @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. */
26052
26356
  recentTickets(from, to, opts) {
26053
26357
  return recentTickets(this, from, to, this.http, opts);
26054
26358
  }
@@ -26181,6 +26485,8 @@ export {
26181
26485
  validateFilter,
26182
26486
  buildFilter,
26183
26487
  normalizedConditions,
26488
+ normalizeInValues,
26489
+ assertNoRelationLeaf,
26184
26490
  buildUrl,
26185
26491
  cursorToPath,
26186
26492
  buildQueryBody,
@@ -26213,16 +26519,7 @@ export {
26213
26519
  FIELD_PRESETS,
26214
26520
  FIELD_PRESET_NAMES,
26215
26521
  resolveFieldsPreset,
26216
- BaseResource,
26217
- BillingCodesResource,
26218
- BillingItemsResource,
26219
- isDateYMD,
26220
- monthsInWindow,
26221
- dayAfter,
26222
- num,
26223
- MODEL_FIELD_SETS,
26224
26522
  MODEL_DATE_FIELDS,
26225
- validateQueryFieldNames,
26226
26523
  EXPAND_RESOURCE_NAMES_MAX_IDS,
26227
26524
  expandResourceNames,
26228
26525
  ENTITY_TYPE_TO_RESOURCE,
@@ -26240,6 +26537,18 @@ export {
26240
26537
  vendorId,
26241
26538
  resolveLineOfBusinessChain,
26242
26539
  resolveCompositeNames,
26540
+ readNamesOpt,
26541
+ labelRows,
26542
+ BaseResource,
26543
+ BillingCodesResource,
26544
+ BillingItemsResource,
26545
+ isDateYMD,
26546
+ monthsInWindow,
26547
+ dayAfter,
26548
+ num,
26549
+ collectFilterFieldNames,
26550
+ validateQueryFieldNames,
26551
+ findUnknownFieldNames,
26243
26552
  resolveTicketStatusSemantics,
26244
26553
  openStatusFilter,
26245
26554
  isClosedStatus,