node-autotask 0.34.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -1
- package/MCP_TOOL_CATALOG.json +26 -9
- package/MCP_TOOL_MANIFEST.md +14 -13
- package/N8N_MCP_MANIFEST.json +5 -5
- package/N8N_MCP_MANIFEST.md +4 -4
- package/README.md +12 -5
- package/capabilities.json +3097 -1344
- package/capabilities.schema.json +4 -0
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.d.cts +2 -0
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-P6YN2POI.cjs → chunk-25VGFT5M.cjs} +143 -143
- package/dist/{chunk-EZKM4HQ6.js → chunk-7IADHNBZ.js} +8486 -3122
- package/dist/{chunk-RDFL5QYE.js → chunk-7WI7FBPB.js} +12 -12
- package/dist/{chunk-FOOHJBPP.cjs → chunk-HBYKTXN3.cjs} +3096 -1343
- package/dist/{chunk-4MUBUSTL.js → chunk-KQ6HMBB4.js} +3096 -1343
- package/dist/{chunk-DKJP73FX.cjs → chunk-PFFW4DIK.cjs} +4657 -9597
- package/dist/{chunk-6ADFCU3I.js → chunk-PFJAMFUL.js} +4456 -9396
- package/dist/{chunk-GDQ4QM7Q.cjs → chunk-TBFXRNNE.cjs} +8452 -3088
- package/dist/{index-ByMU8IC2.d.ts → index-BDqw377y.d.ts} +117 -14
- package/dist/{index-Dx4-zpDo.d.cts → index-CqepIQfp.d.cts} +117 -14
- package/dist/index.cjs +22 -22
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -14
- package/dist/operations/index.cjs +828 -137
- package/dist/operations/index.d.cts +145 -5
- package/dist/operations/index.d.ts +145 -5
- package/dist/operations/index.js +697 -6
- package/dist/pool.cjs +9 -9
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +5 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +14 -13
- package/docs/API.md +331 -13
- package/docs/LIVE-TESTING.md +11 -11
- package/package.json +1 -1
package/docs/API.md
CHANGED
|
@@ -270,6 +270,7 @@ status → what the vendor does that triggers it.
|
|
|
270
270
|
| `DeleteBoundaryError` | `DELETE_BOUNDARY` | policy | — | `operations.recovery` `action: 'delete'` on a resource the vendored spec defines no DELETE for — refused PRE-SEARCH (zero wire; the boundary is the spec-derived plan audit, never a raw vendor 405; issue #9) |
|
|
271
271
|
| `MarkerUnsafeIdError` | `MARKER_UNSAFE_ID` | policy | — | a `operations.recovery` match whose id is beyond `Number.MAX_SAFE_INTEGER` (unaddressable through the typed delete surface) — refused with the typed code, never a silent skip (issue #9, P1) |
|
|
272
272
|
| `RecoveryTruncatedError` | `RECOVERY_TRUNCATED` | policy | — | the `operations.recovery` marker search tripped its match cap (more matches than `limit`) — refused instead of a silent truncation / silent empty (issue #9) |
|
|
273
|
+
| `AutotaskError` | `BUDGET_EXCEEDED` | policy | — (the call that would exceed it is not sent) | `operations.revenueRollup`: the pre-walk estimate exceeds `maxVendorCalls` (refused before any stream; `details: { phase: 'estimate', estimate, budget, suggestions }`) or the counting wrapper reached the budget mid-walk (`phase: 'walk'`; the partial fold is discarded) — `suggestedAction` names the narrowing (issue #234) |
|
|
273
274
|
|
|
274
275
|
Retryable by contract: `NETWORK_ERROR`, `TIMEOUT`, `RATE_LIMITED`,
|
|
275
276
|
`SERVER_ERROR` (429/5xx) — with ONE exception: a deterministic vendor
|
|
@@ -619,7 +620,11 @@ without the username). Consequences:
|
|
|
619
620
|
evicted-then-recreated tenant on the same host hits the same state).
|
|
620
621
|
`store` injects a custom `CircuitBreakerStore` (the same get/set/delete
|
|
621
622
|
shape as `CooldownStore`, over the richer per-key state
|
|
622
|
-
`{ failures, openedAt, successes, probes }
|
|
623
|
+
`{ failures, openedAt, successes, probes, windowOpenedAt? }`;
|
|
624
|
+
`windowOpenedAt` is the open-window identity a probe's settlement is
|
|
625
|
+
pinned to — persist it as-is, and keep every process sharing a store on
|
|
626
|
+
the same version, since a build that re-arms an entry without it leaves
|
|
627
|
+
newer pinned probes voided with their slot unreleased).
|
|
623
628
|
|
|
624
629
|
**States (timer-free — derived from `Date.now()` lazily at each request,
|
|
625
630
|
exactly like the #6 cooldown):** `closed` (no entry / nothing armed — a
|
|
@@ -1106,13 +1111,13 @@ be cross-mode-refused.
|
|
|
1106
1111
|
and the `confirm === <op key>` gate (all 54 deletes; the 3 approval-gated
|
|
1107
1112
|
writes — inside `autotask_write` too) apply exactly as in `invoke`.
|
|
1108
1113
|
Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
|
|
1109
|
-
all
|
|
1114
|
+
all 1281 ops × 3 modes, zero wire).
|
|
1110
1115
|
|
|
1111
1116
|
**Harness approval policy (recommended).** Require approval on every tool
|
|
1112
1117
|
whose MCP annotation is `readOnlyHint !== true` — today exactly **267
|
|
1113
1118
|
tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
|
|
1114
1119
|
`effect=destructive`, every one of them has a dedicated tool) +
|
|
1115
|
-
`autotask_write` + `autotask_delete`. The remaining **
|
|
1120
|
+
`autotask_write` + `autotask_delete`. The remaining **635 tools** (the 622 dedicated read tools + the 10 core read tools + `autotask_read` +
|
|
1116
1121
|
`autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
|
|
1117
1122
|
and are safe ungated: every op they can route is `effect=read` (zero
|
|
1118
1123
|
mutating requests — pinned by the test inventory, §6 of the split design).
|
|
@@ -1563,10 +1568,201 @@ The secondary read re-applies the open filter client-side, and `limit` is not
|
|
|
1563
1568
|
validated (it does not apply). With `scope` `primary`/`secondary` only that side is
|
|
1564
1569
|
read and `both` is 0.
|
|
1565
1570
|
|
|
1571
|
+
### 7.14 `revenueRollup(client, opts)` — revenue by dimension, budgeted (issues #234, #235, #236)
|
|
1572
|
+
|
|
1573
|
+
One read call answers "revenue for Recurring Service + Fixed Price contracts
|
|
1574
|
+
over a window, by account vertical": the registry op `operations.revenueRollup`
|
|
1575
|
+
(MCP tool `autotask_revenue_rollup`, tier extended).
|
|
1576
|
+
|
|
1577
|
+
- **Scope** (exactly one, REQUIRED — no scope is refused with zero wire):
|
|
1578
|
+
`companyIDs` | `ownerResourceID` (resolved to company ids by one projected
|
|
1579
|
+
`companies` query) | `contractIDs` | `tenantWide: true`. A company scope
|
|
1580
|
+
filters on the LINE's `companyID` (attribution is by the line's company).
|
|
1581
|
+
- **Window**: `dateFrom` / `dateTo` (`YYYY-MM-DD`, both REQUIRED, inclusive,
|
|
1582
|
+
whole UTC days, at most 36 months), invoice-date basis.
|
|
1583
|
+
The window is whole calendar days sent to the vendor as bare `YYYY-MM-DD`
|
|
1584
|
+
bounds; that was verified on a tenant ahead of UTC only, and tenants behind
|
|
1585
|
+
UTC are unverified.
|
|
1586
|
+
- **Revenue** = Σ `billing_items.internalCurrencyTotalAmount` (ex-tax, internal
|
|
1587
|
+
currency) over the lines of NON-VOIDED invoices (`isVoided eq false`
|
|
1588
|
+
server-side) in the window, folded in integer cents.
|
|
1589
|
+
`includedValue` = Σ(`internalCurrencyExtendedPrice` − total) is
|
|
1590
|
+
informational and never added to revenue. Credits (negative lines) are
|
|
1591
|
+
netted and also reported per row. A line whose `internalCurrencyTotalAmount`
|
|
1592
|
+
/ `internalCurrencyExtendedPrice` is missing or null falls back to the
|
|
1593
|
+
local-currency `totalAmount` / `extendedPrice` (as contract profitability
|
|
1594
|
+
does), everywhere those fields are read (revenue, `includedValue`, the
|
|
1595
|
+
non-chargeable class and value); never silently: one
|
|
1596
|
+
`coverage.blindSurfaces` entry gives the count of such lines.
|
|
1597
|
+
- **`contractTypes`** (ids or exact labels) default to the live picklist
|
|
1598
|
+
labels that START with `Recurring` or `Fixed` (case-insensitive,
|
|
1599
|
+
`/^\s*(recurring|fixed)\b/i`: `Non-Recurring` does not match); the resolved map is echoed in
|
|
1600
|
+
`coverage.contractTypeMap`. No match is a `CONFIG_ERROR` listing the live
|
|
1601
|
+
labels and ids. `contractCategories` filters the same way, and when given
|
|
1602
|
+
its resolved map is echoed in `coverage.contractCategoryMap` (`{ "<id>":
|
|
1603
|
+
label }`, absent otherwise). A label shared by several live picklist ids
|
|
1604
|
+
selects every one of them.
|
|
1605
|
+
- **`groupBy`** (default `['vertical', 'contractType']`): `vertical`
|
|
1606
|
+
(`companies.marketSegmentID`), `contractType`, `contractCategory`,
|
|
1607
|
+
`company`, `month` (the invoice month), `billingItemKind`, and the join
|
|
1608
|
+
dimensions (#235):
|
|
1609
|
+
- `contract` — `{ id, name }` (`contractName` joins the contract fetch).
|
|
1610
|
+
- `lineOfBusiness` — per billing line (`organizationalLevelAssociationID`;
|
|
1611
|
+
no contract-pointer fallback): three batch `id in` fetches (associations,
|
|
1612
|
+
level1s, level2s) give `{ id: associationId, name: level2Name (the Line of
|
|
1613
|
+
Business), division: level1Name }`. Lines without a pointer form their own
|
|
1614
|
+
row `{ id: null, label: 'no LOB assigned' }`, and
|
|
1615
|
+
`coverage.lineOfBusiness = { noPointer: { amount, lines }, note }` carries
|
|
1616
|
+
their amount (LOB figures are floors). A row's `contracts` is "contracts
|
|
1617
|
+
with at least one line in this LOB", so per-LOB counts can sum to more than
|
|
1618
|
+
`totals.contracts`.
|
|
1619
|
+
- `resource` — labour lines only (`billingItemType` Labor / Labor
|
|
1620
|
+
Adjustment, or the FK fallback): `timeEntryID` → time entry → resource,
|
|
1621
|
+
`{ id: resourceID, name: 'First Last' }`. Non-labour lines, and labour
|
|
1622
|
+
lines with no time entry (e.g. an adjustment), are `{ id: null, label: 'n/a' }`.
|
|
1623
|
+
- `billingCode` — `billingCodeID` → billing code `{ id, name }`; a line
|
|
1624
|
+
without one is `{ id: null, label: 'none' }`.
|
|
1625
|
+
|
|
1626
|
+
Each row key is `{ id, label?, name?, division? }`: picklist text in
|
|
1627
|
+
`label`; vendor entity text (untrusted — company, contract, LOB level2,
|
|
1628
|
+
resource and billing-code names) in `name`, and the LOB division (level1
|
|
1629
|
+
name, also untrusted) in `division`. Missing values get their own row
|
|
1630
|
+
(`unassigned`, `uncategorised`, `no LOB assigned`, `n/a`, `none`); a value
|
|
1631
|
+
the picklist does not serve stays id-only and is listed in
|
|
1632
|
+
`coverage.blindSurfaces`. A join id the `id in` fetch does not return is
|
|
1633
|
+
never guessed: the line goes to `coverage.excluded.unresolvedAssociations`
|
|
1634
|
+
(association or its level1/level2), `unresolvedTimeEntries` (time entry or
|
|
1635
|
+
its resource) or `unresolvedBillingCodes`, with its amount. These buckets
|
|
1636
|
+
(and `coverage.lineOfBusiness`) appear only when their dimension is grouped;
|
|
1637
|
+
the line projection grows by `organizationalLevelAssociationID`,
|
|
1638
|
+
`timeEntryID` / the type fields and `billingCodeID` the same way.
|
|
1639
|
+
- **Rows** are sorted by revenue descending, then ids; `top` (default 100,
|
|
1640
|
+
max 100) keeps the head and folds the tail into `rowsTruncated`. `totals`
|
|
1641
|
+
always cover every row.
|
|
1642
|
+
- **`coverage.excluded`**: voided invoices (count + header amount; under a
|
|
1643
|
+
company / owner scope only the voided invoices whose HEADER `companyID` is
|
|
1644
|
+
in scope; omitted for a `contractIDs` scope, since an invoice header cannot
|
|
1645
|
+
be attributed to a contract, and noted in `coverage.notes`), uncontracted
|
|
1646
|
+
lines, other contract types (per label), other contract categories (per
|
|
1647
|
+
label; a label shared by several live ids is keyed `label (id)`, so ids
|
|
1648
|
+
are never merged) and
|
|
1649
|
+
unresolved contracts / companies (ids the `id in` fetch did not return; an
|
|
1650
|
+
unresolved contract is never counted as uncontracted), each with an amount
|
|
1651
|
+
and a line count. Every line is in exactly one place, so `totals.revenue` +
|
|
1652
|
+
Σ the excluded line buckets = Σ the lines fetched (lines are de-duplicated
|
|
1653
|
+
by id before the fold).
|
|
1654
|
+
- **`coverage.zeroTotalInvoices`** `{ count, amount, lines }` is an
|
|
1655
|
+
informational overlay, NOT an excluded bucket: the zero-total invoices in
|
|
1656
|
+
scope (tenant-wide: all; company / owner scope: header company in scope or
|
|
1657
|
+
carrying an in-scope line; `contractIDs`: carrying a scoped line) and the
|
|
1658
|
+
amount / count of their lines. Those lines are still folded into rows or the
|
|
1659
|
+
excluded buckets.
|
|
1660
|
+
- **`includeNonChargeable`** (#236, default `false`; when `false` the call,
|
|
1661
|
+
its wire and its output are byte-identical to leaving it out): adds
|
|
1662
|
+
`nonChargeable: { contractCovered: { value, hours, lines }, nonBillable:
|
|
1663
|
+
{ value, hours, lines } }` to every row and to `totals`. It is NEVER added to
|
|
1664
|
+
`revenue` / `totals.revenue`. Classes per line: non-billable =
|
|
1665
|
+
`nonBillable` true; contract-covered = billable with
|
|
1666
|
+
`internalCurrencyExtendedPrice` > 0 and `internalCurrencyTotalAmount` = 0.
|
|
1667
|
+
`value` = Σ `internalCurrencyExtendedPrice` (integer cents); `hours` = Σ
|
|
1668
|
+
`quantity` on labour lines, Labor Adjustment (type 2) included (it adjusts
|
|
1669
|
+
labour hours, and `billingLineType` already classes it as labour). The
|
|
1670
|
+
invoiced part comes from the revenue line stream (+ `nonBillable`,
|
|
1671
|
+
`quantity` and the type fields); the uninvoiced part is a second unsliced
|
|
1672
|
+
stream `invoiceID notExist` with `itemDate` in the window (+ the same
|
|
1673
|
+
scope). Uninvoiced lines have no invoice date, so they are windowed and
|
|
1674
|
+
month-keyed on `itemDate` (stated in `coverage.notes`). They are keyed
|
|
1675
|
+
through the same joins (one fetch over both streams, so only ids the
|
|
1676
|
+
invoiced lines did not already need are added); uninvoiced lines that are
|
|
1677
|
+
uncontracted or of another contract type / category are outside the
|
|
1678
|
+
request. A non-chargeable line, invoiced or uninvoiced, whose row cannot be
|
|
1679
|
+
resolved (an unresolved contract / company or a join miss) goes to
|
|
1680
|
+
`coverage.excluded.unresolvedNonChargeable` (amount = Σ value); an invoiced
|
|
1681
|
+
one is also in its revenue excluded bucket. The default `maxVendorCalls` becomes 150 (an explicit
|
|
1682
|
+
value wins; cap 200).
|
|
1683
|
+
- **`coverage.reconciliation`**: header `invoiceTotal` vs Σ line `totalAmount`
|
|
1684
|
+
per invoice (tolerance 0 cents; `totalAmount`, so a multi-currency tenant
|
|
1685
|
+
gives no false delta). Tenant-wide it covers every invoice; a company scope
|
|
1686
|
+
covers the invoices whose header company is in scope; a `contractIDs`
|
|
1687
|
+
scope skips it (noted).
|
|
1688
|
+
- **Budget** (`maxVendorCalls`, default 90, max 200): every vendor call goes
|
|
1689
|
+
through a counting wrapper. Before any stream the op counts the window's
|
|
1690
|
+
non-voided invoices and the invoiced lines (`itemDate` in the window, a
|
|
1691
|
+
proxy for the lines of those invoices) and estimates
|
|
1692
|
+
`calls so far + 1 (voided; not for a contractIDs scope) + invoice pages + line pages (one per 500-invoice
|
|
1693
|
+
chunk + overflow) + one line count per 500-invoice chunk (the per-chunk line gate) + uninvoiced pages (includeNonChargeable: its count is in
|
|
1694
|
+
"calls so far", ≥ 1 page) + each join's id-fetch chunks`. A join's chunks
|
|
1695
|
+
default to `ceil(min(lines + uninvoiced, invoices) / 500)` (contracts,
|
|
1696
|
+
companies, LOB associations, billing codes); time entries use
|
|
1697
|
+
`ceil((lines + uninvoiced) / 500)` (one per labour line, the labour share is
|
|
1698
|
+
unknown pre-walk); LOB level1s / level2s and resources count one chunk each
|
|
1699
|
+
(small org / staff tables). Over budget it REFUSES with `BUDGET_EXCEEDED`
|
|
1700
|
+
(`details: { phase: 'estimate', estimate, budget, suggestions }`, the
|
|
1701
|
+
message names the narrowings that apply: `maxVendorCalls: N` (while N is
|
|
1702
|
+
within the cap), a later `dateFrom`, a scope, and, when raising the budget
|
|
1703
|
+
cannot help (estimate past the cap), `includeNonChargeable: false`, fewer
|
|
1704
|
+
`groupBy` dimensions, `contractIDs`, or a shorter id list (only for a list of
|
|
1705
|
+
more than one id); at least one suggestion is always present. Only a raised
|
|
1706
|
+
`maxVendorCalls` and a later `dateFrom` carry an estimate: the invoice
|
|
1707
|
+
stream is tenant-wide in every scope and the line stream costs at least one
|
|
1708
|
+
page and one count per chunk of 500 of those invoices, so a scope or shape
|
|
1709
|
+
narrowing does not reduce that part, and when the refusal offers one its
|
|
1710
|
+
message says it may still be refused; when nothing narrower exists the
|
|
1711
|
+
only suggestion is the cap, and it says the call will be refused at that
|
|
1712
|
+
estimate even at the cap). A budget
|
|
1713
|
+
below the sizing overhead (the labels, owner query and count calls that run
|
|
1714
|
+
before the estimate exists: at least 4 calls for the default shape, more with
|
|
1715
|
+
`ownerResourceID`, `includeNonChargeable` or more picklist dimensions) is
|
|
1716
|
+
the same refusal (`phase: 'estimate'`), with `estimate` = that minimum and
|
|
1717
|
+
`suggestions: [{ maxVendorCalls }]`; the full estimate is named once the
|
|
1718
|
+
call is sized.
|
|
1719
|
+
Mid-walk the wrapper throws `BUDGET_EXCEEDED` (`phase: 'walk'`) before the
|
|
1720
|
+
call that would exceed the budget. The wrapper counts LOGICAL requests: the
|
|
1721
|
+
transport retries a 429 / 5xx internally (up to `maxRetries` each) below
|
|
1722
|
+
it, and those retries are not counted in the budget, `vendorCalls` or
|
|
1723
|
+
`tenantHourlyShare`. A 429 propagates as `RATE_LIMITED`. These fail as
|
|
1724
|
+
`INVALID_RESPONSE`: a blind gate count (`reason: 'blind_count'`); the invoice
|
|
1725
|
+
stream, each invoice-chunk line stream and the uninvoiced stream
|
|
1726
|
+
(`includeNonChargeable`) whose DISTINCT ids differ from their gate count
|
|
1727
|
+
(`reason: 'count_mismatch'`, `details.stream` names it: a page that repeats
|
|
1728
|
+
one row and drops another is caught; each line chunk is counted on exactly
|
|
1729
|
+
its stream's filter); and a line whose invoice is not among the streamed
|
|
1730
|
+
invoices (`reason: 'orphan_line'`). Retry on a count race (data that changes
|
|
1731
|
+
mid-walk). A repeated voided header is counted once, but a DROPPED voided
|
|
1732
|
+
header is undetectable (there is no voided count call), so
|
|
1733
|
+
`coverage.excluded.voidedInvoices` is a floor under vendor paging drift. Every fault discards the fold: a result is always
|
|
1734
|
+
complete. `coverage.vendorCalls` is the exact logical-request count (joins
|
|
1735
|
+
included; transport retries excluded); ~50
|
|
1736
|
+
calls for a 12-month tenant-wide report on the reference tenant, ~115 with
|
|
1737
|
+
`includeNonChargeable`. `resource` on 12 months tenant-wide adds ~15-25
|
|
1738
|
+
calls and can refuse at the default 90 (the refusal names N).
|
|
1739
|
+
An `ownerResourceID` that owns more than 100 companies is a `CONFIG_ERROR`
|
|
1740
|
+
after at most one page past the 100th id (the resolution stops there).
|
|
1741
|
+
- **Limits**: amounts are rounded to cents per line (`Math.round(x * 100)`)
|
|
1742
|
+
before summing, on the assumption that the vendor sends at most 2 decimals;
|
|
1743
|
+
a 3-decimal amount can drift, and reconciliation (header vs line totals)
|
|
1744
|
+
flags the delta. Non-chargeable `hours` are rounded to hundredths per line
|
|
1745
|
+
before summing. A row that holds only non-chargeable work has revenue 0, so
|
|
1746
|
+
it sorts above rows with negative revenue (net credits). The estimate costs
|
|
1747
|
+
the voided stream as one page and assumes distinct contracts / companies
|
|
1748
|
+
≤ invoices; a window with more than 500 voided invoices, or (with
|
|
1749
|
+
`includeNonChargeable`) many uninvoiced lines on contracts that never
|
|
1750
|
+
invoiced in the window, can exceed it, and such a call fails mid-walk with
|
|
1751
|
+
`BUDGET_EXCEEDED` (`phase: 'walk'`) rather than returning a partial result.
|
|
1752
|
+
|
|
1753
|
+
```ts
|
|
1754
|
+
import { revenueRollup } from 'node-autotask/operations';
|
|
1755
|
+
const r = await revenueRollup(client, { tenantWide: true, dateFrom: '2025-07-01', dateTo: '2026-06-30' });
|
|
1756
|
+
// r.rows[0] → { keys: { vertical: { id: 12, label: 'Retail' }, contractType: { id: 7, label: 'Recurring Service' } }, revenue, credits, includedValue, contracts, items }
|
|
1757
|
+
const lob = await revenueRollup(client, { tenantWide: true, dateFrom: '2025-07-01', dateTo: '2026-06-30',
|
|
1758
|
+
contractTypes: ['Recurring Service'], groupBy: ['vertical', 'lineOfBusiness'], includeNonChargeable: true });
|
|
1759
|
+
// lob.rows[i].keys.lineOfBusiness → { id: 7, name: '<level2>', division: '<level1>' } | { id: null, label: 'no LOB assigned' }
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1566
1762
|
## 8. Resource surface (135 resources)
|
|
1567
1763
|
|
|
1568
1764
|
<!-- BEGIN GENERATED:api-per-entity -->
|
|
1569
|
-
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable
|
|
1765
|
+
Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 16). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
|
|
1570
1766
|
|
|
1571
1767
|
| resource key | apiEntity | kind | tier | parent | standard ops present | special ops | mcpTool |
|
|
1572
1768
|
|--------------|-----------|------|------|--------|----------------------|-------------|---------|
|
|
@@ -2141,6 +2337,64 @@ own. A match that `get(id)` would return can be invisible to the lookup, so a
|
|
|
2141
2337
|
missed dedup can issue a duplicate create; treat dedup there as best-effort
|
|
2142
2338
|
until the zone is re-verified.
|
|
2143
2339
|
|
|
2340
|
+
### 11.8a Structured `zoneFacts.quirks` entries (issue #253)
|
|
2341
|
+
|
|
2342
|
+
The per-resource `quirks` on the `zoneFacts` block (§11.7) is structured, not
|
|
2343
|
+
free text — a `string[]` of campaign prose (dates, tenant names, operator
|
|
2344
|
+
advice) cannot ride to a model, but a clean statement + a filterable scope
|
|
2345
|
+
can. Each entry (`StructuredQuirk`):
|
|
2346
|
+
|
|
2347
|
+
- `kind` — the closed 13-set (unknown kinds fail `capabilities:check`, clause
|
|
2348
|
+
23, listing the valid set): `not-persisted` · `blind-surface` ·
|
|
2349
|
+
`integer-picklist` · `wire-type` · `required-in-body` · `not-in-body` ·
|
|
2350
|
+
`parent-scoped` · `field-constraint` · `vendor-managed-side-effect` ·
|
|
2351
|
+
`cascade-delete` · `access-denied` · `unknown-field-noop` · `capability-note`
|
|
2352
|
+
— four of the set are extensions forced by specific migrated strings:
|
|
2353
|
+
`not-in-body` (the scoped-path-vs-body distinction), `field-constraint`
|
|
2354
|
+
(live-verified VALUE constraints), `unknown-field-noop` (the vendor's
|
|
2355
|
+
silent 200 on unknown fields), and `capability-note` (affirmative
|
|
2356
|
+
capabilities).
|
|
2357
|
+
Adding a kind later = one-line extension of the clause's `QUIRK_KINDS` + a
|
|
2358
|
+
plan comment citing the issue; the clause id stays stable.
|
|
2359
|
+
- `ops` — the ops the fact applies to: the closed set `{count, create, query,
|
|
2360
|
+
update, delete, all}` plus the host's own plan `specialOps` (membership
|
|
2361
|
+
against the plan — the set never hard-codes what the plan already declares).
|
|
2362
|
+
A server about to execute `billing_codes.count` keeps exactly the entries
|
|
2363
|
+
whose `ops` include `count`. `archive` is deliberately not in the set (no
|
|
2364
|
+
archive verb in the plan's op vocabulary).
|
|
2365
|
+
- `fields` — optional; the specific wire/model fields the fact constrains
|
|
2366
|
+
(no membership check — names like `internalBillingCodeID` are by definition
|
|
2367
|
+
outside the vendored spec).
|
|
2368
|
+
- `statement` — the clean model-facing sentence: tenant-neutral, date-free,
|
|
2369
|
+
instruction-free.
|
|
2370
|
+
- `evidence` — the verbatim campaign string (the provenance home; frozen and
|
|
2371
|
+
exempt from the hygiene gate — the same standing as `vendorEvidence` on the
|
|
2372
|
+
same surface).
|
|
2373
|
+
|
|
2374
|
+
**The clause 23 hygiene gate**
|
|
2375
|
+
(`scripts/lib/checks/clause23-structured-quirks.mjs`): a build clause that
|
|
2376
|
+
fails when a `statement` carries one of the enforced provenance pattern
|
|
2377
|
+
classes — clause 15's `PROVENANCE_PATTERNS` (campaign markers, tenant ids,
|
|
2378
|
+
branch names, ISO and short dates, 6+ digit ids, …) plus the #253 overrides
|
|
2379
|
+
(`live-verified` / `live-proven` / `proven` / `unproven`, `S9b`-style run
|
|
2380
|
+
codes, `UI-only`, `ask the operator|admin|vendor`, `australia-nz`, `node
|
|
2381
|
+
rule`, slash dates and month-name dates in both orders, `last`/`next`/`this`
|
|
2382
|
+
+ a relative unit, ordinals like `the 21st`, `tenant` + a 5+ digit id,
|
|
2383
|
+
`cleanup`, and zone codes like `ww29` even mid-token) — or when an entry is
|
|
2384
|
+
shape-invalid (unknown kind, an op outside the closed set, an empty
|
|
2385
|
+
statement/evidence). The gate is a PATTERN FLOOR: it enforces the listed
|
|
2386
|
+
pattern classes; it is not a natural-language guarantee, so a well-formed
|
|
2387
|
+
date, tenant name or operator instruction in an unlisted shape can slip.
|
|
2388
|
+
`evidence` is NEVER gated. The 2026-10-28 migration replaced the old 11-host
|
|
2389
|
+
`string[]` in place: 28 raw strings → 28 structured entries, every old
|
|
2390
|
+
string preserved verbatim as its entry's `evidence`.
|
|
2391
|
+
|
|
2392
|
+
**Consumer contract** (served via `relationshipMap(k).zoneFacts.quirks`,
|
|
2393
|
+
§9.1): filter entries on `ops`, feed `entry.statement` to the model, and
|
|
2394
|
+
treat `entry.evidence` with the same trust you already apply to
|
|
2395
|
+
`vendorEvidence` on this surface (plan-grade provenance, not model-facing
|
|
2396
|
+
prose). The old `string[]` shape is gone.
|
|
2397
|
+
|
|
2144
2398
|
### 11.9 Delete-boundary awareness + the one-call recovery (issue #9)
|
|
2145
2399
|
|
|
2146
2400
|
The §11.1 wall (7 live-verified 405 endpoints) is now a **spec-derived
|
|
@@ -2450,12 +2704,14 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2450
2704
|
condition is ever sent. The join is one `BillingItems` query per 250 ids;
|
|
2451
2705
|
the entry read is capped at 500, so a call costs at most 1 read + 2 joins
|
|
2452
2706
|
and a window with more than 500 entries returns the first 500 with
|
|
2453
|
-
`hasMore` / `truncated` true
|
|
2707
|
+
`hasMore` / `truncated` true and a `nextCursor` to continue from (or use
|
|
2708
|
+
`countOnly` for the window's totals; see the paging bullet below). A 429 or vendor fault propagates as a typed
|
|
2454
2709
|
error. The API user therefore needs BillingItems read permission; a permission fault propagates as a typed error.
|
|
2455
2710
|
- **`time_entries.getPosted(opts?)` / `getUnposted(opts?)`** — `opts` is
|
|
2456
|
-
`{ limit?, ticketID?, dateFrom?, dateTo? }`
|
|
2711
|
+
`{ limit?, ticketID?, dateFrom?, dateTo?, cursor?, countOnly?, maxVendorCalls? }`
|
|
2712
|
+
(the paging options: see the paging bullet below; `dateFrom`/`dateTo`:
|
|
2457
2713
|
inclusive `dateWorked` days, `YYYY-MM-DD`, never tz-shifted). They return
|
|
2458
|
-
`{ items, hasMore, window, approvedCount, unapprovedCount }` (a breaking
|
|
2714
|
+
`{ items, hasMore, window, nextCursor, unconfirmed, approvedCount, unapprovedCount }` (a breaking
|
|
2459
2715
|
change from the bare `TimeEntry[]`, 0.32.0): `getPosted` items are entries
|
|
2460
2716
|
a billing item references; `getUnposted` items are entries none references.
|
|
2461
2717
|
`approvedCount` / `unapprovedCount` are informational (items with / without
|
|
@@ -2468,7 +2724,7 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2468
2724
|
guard forbids Date positionals), one bounded read (`limit` 1-500, default
|
|
2469
2725
|
500 — the hard cap, issue #128: a larger window needs `time_entries.query`
|
|
2470
2726
|
with the same `dateWorked` filter, its 2500-row walk cap) returning
|
|
2471
|
-
`{ items, postedCount, unpostedCount, window, truncated }`; the counts come
|
|
2727
|
+
`{ items, postedCount, unpostedCount, window, nextCursor, unconfirmed, truncated }`; the counts come
|
|
2472
2728
|
from the billing-item join (any billing item referencing the entry), `opts.posted` narrows client-side after the join
|
|
2473
2729
|
(true = posted only, false = unposted only, omitted = both); the counts still split every entry read (before the narrowing), so `items.length` can differ from their sum and `ticketID` /
|
|
2474
2730
|
`resourceID` narrow the read. Live evidence for the visibility class
|
|
@@ -2490,6 +2746,57 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
|
|
|
2490
2746
|
but, since 0.24.0, it never shifts the bounds: `dateWorked` is a date-only
|
|
2491
2747
|
value at 00:00 UTC, so `from`/`to` are the days (see §7.11). The same `tz`
|
|
2492
2748
|
opt is on every date-window op — see §11.22.
|
|
2749
|
+
- **Paging past 500 entries and whole-window totals (issue #242)** — all four
|
|
2750
|
+
ops (`getPosted`, `getUnposted`, `postedWithin`, `dateWorkedWindow`) take
|
|
2751
|
+
three more options:
|
|
2752
|
+
- `cursor?: string` — every rows result now carries `nextCursor: string | null`
|
|
2753
|
+
(the vendor's next-page cursor for the entry read). Pass it back as
|
|
2754
|
+
`opts.cursor` with the **same** window and filters to read the next page;
|
|
2755
|
+
each call stays one entry page (the vendor's continuation page, up to 500
|
|
2756
|
+
entries; `limit` sizes the first page only) plus at most 2 joins. Loop
|
|
2757
|
+
until `nextCursor` is `null` to enumerate a busy window. An off-origin
|
|
2758
|
+
cursor, or a same-origin cursor whose path is not the TimeEntries
|
|
2759
|
+
continuation (`/V1.0/TimeEntries/query/next`), or a cursor longer than
|
|
2760
|
+
2,048 characters, is refused (`RESPONSE_REJECTED`) before any request. Every rows result also carries
|
|
2761
|
+
`unconfirmed: number`: the entries on this page whose billing-item join
|
|
2762
|
+
page was truncated. They are withheld from `items` (getPosted /
|
|
2763
|
+
getUnposted) and from both counts (postedWithin / dateWorkedWindow, which
|
|
2764
|
+
also withhold them from `items` when `posted` is set; with `posted`
|
|
2765
|
+
omitted they stay in `items`, unclassified), so a cursor loop does not
|
|
2766
|
+
see them as posted or unposted; `unconfirmed` is 0 normally, and when it is
|
|
2767
|
+
above 0 `hasMore` / `truncated` is also true. Sum it across a loop to know
|
|
2768
|
+
how many entries went unclassified (or use `countOnly`, which refuses
|
|
2769
|
+
instead).
|
|
2770
|
+
- `countOnly?: true` — returns only the whole window's totals:
|
|
2771
|
+
`{ window, total, postedCount, unpostedCount, vendorCalls, estimate, budget }`
|
|
2772
|
+
(typed overloads select `TimeEntriesPostedCountResult`). One
|
|
2773
|
+
`TimeEntries/query/count` call sizes the window; then every entry page is
|
|
2774
|
+
read ids-only (`IncludeFields: ['id']`, following the vendor cursor) and
|
|
2775
|
+
joined at 250 ids per join, and the window is counted again after the walk.
|
|
2776
|
+
`total` = `postedCount + unpostedCount` (de-duplicated by id). The call
|
|
2777
|
+
fails with `INVALID_RESPONSE` (`details: { reason: 'count_mismatch',
|
|
2778
|
+
streamed, counted, recounted }`) when the distinct-id total equals NEITHER
|
|
2779
|
+
the count before the walk NOR the count after it (a page repeated one entry
|
|
2780
|
+
and dropped another); a total equal to either count is accepted, so a
|
|
2781
|
+
window that grows or shrinks by the time the walk ends still succeeds, and
|
|
2782
|
+
one that changes mid-walk can fail or pass depending on timing: retry.
|
|
2783
|
+
`limit` and `posted` do not apply; `cursor` is rejected with `countOnly`
|
|
2784
|
+
(CONFIG_ERROR).
|
|
2785
|
+
- `maxVendorCalls?: number` — the `countOnly` budget (integer 1-200,
|
|
2786
|
+
default 30). The estimate is `1 + max(1, ceil(n / 500)) + ceil(n / 250) + 1`
|
|
2787
|
+
(the last 1 is the re-count) from the count `n` (the live week of 1,321 entries costs 11). The budget
|
|
2788
|
+
and `vendorCalls` count logical requests; transport retries of a 429 /
|
|
2789
|
+
5xx (up to `maxRetries` each) are not counted. Over budget
|
|
2790
|
+
the call throws `BUDGET_EXCEEDED` (category `policy`, `details: { estimate,
|
|
2791
|
+
budget, total, issued }`, a `suggestedAction` naming a narrower window or
|
|
2792
|
+
`maxVendorCalls >= estimate`) after the count call and before any page
|
|
2793
|
+
read. The same code stops a walk that needs more calls than estimated
|
|
2794
|
+
(more entries than counted, or an extra page such as an empty
|
|
2795
|
+
continuation page) before the call that would exceed the budget, the
|
|
2796
|
+
closing re-count included; its `suggestedAction` names a narrower window
|
|
2797
|
+
or a `maxVendorCalls` above the budget, never `>= estimate`. A
|
|
2798
|
+
truncated join throws `RESOLUTION_TRUNCATED`, and a 429 propagates as
|
|
2799
|
+
`RATE_LIMITED`: the total is complete or not returned.
|
|
2493
2800
|
- **Drifted-create marker recovery (issue #13)** — where the plan pins a
|
|
2494
2801
|
`responseDrift` row (the pinned resource: `ticket_checklist_libraries`,
|
|
2495
2802
|
the W-A7 drift: the vendor answers 200 with a body that is not
|
|
@@ -2587,10 +2894,17 @@ there is no second entity-type table.
|
|
|
2587
2894
|
parallel field system, and the served row states it rather than leaving it
|
|
2588
2895
|
inferred. With the `udf` include, the row's `udf` list holds the UDF
|
|
2589
2896
|
DEFINITIONS the vendor's `entityInformation/userDefinedFields` channel
|
|
2590
|
-
returns
|
|
2591
|
-
`
|
|
2592
|
-
`
|
|
2593
|
-
|
|
2897
|
+
returns, projected to `name`, `label` (when non-empty), `type`, `isRequired`,
|
|
2898
|
+
`isReadOnly`, `filterable: 'udf-marker'`, `isPickList`, `maxLength`,
|
|
2899
|
+
`referenceEntityType`, `targetResource`, `targetEnumerable`, and
|
|
2900
|
+
`picklistValues` (the values ride the `labels: true` opt, zero extra wire;
|
|
2901
|
+
present only for a picklist that exposes values). The vendor's `isReference`
|
|
2902
|
+
and `defaultValue` are not projected. A UDF definition never
|
|
2903
|
+
appears in the plan artifact (it is tenant-specific). The row's `udfCapable`
|
|
2904
|
+
is the plan resource flag (16 resources), which every capability record also
|
|
2905
|
+
carries as `CapabilityRecord.udfCapable` (issue #254); every vendor entity has
|
|
2906
|
+
a `getUdfInfo` catalog endpoint, so that op's presence does not make a
|
|
2907
|
+
resource UDF-capable. The row also carries
|
|
2594
2908
|
`udfFilter: 'udf-marker'` and `udfWriteShape: 'merge'` exactly when it is
|
|
2595
2909
|
`udfCapable`: a filter on a UDF NAME is accepted by the vendor only with the
|
|
2596
2910
|
`udf: true` marker, so a UDF field row serves `filterable: 'udf-marker'` and
|
|
@@ -2927,7 +3241,11 @@ what makes it usable as a recovery signal from inside the client.
|
|
|
2927
3241
|
a different host) — the re-pointed host's probe budget / close count is
|
|
2928
3242
|
never touched by a stale-host probe, and the stale host's admitted
|
|
2929
3243
|
budget dangles for the rest of the open period (bounded, conservative,
|
|
2930
|
-
nothing crosses hosts). The
|
|
3244
|
+
nothing crosses hosts). The probe is equally pinned to the open WINDOW
|
|
3245
|
+
it was admitted in (issue #271): a probe that settles after that window
|
|
3246
|
+
closed and re-opened is voided the same way — the newer window's probe
|
|
3247
|
+
budget / close count is never touched by a stale-window probe. The
|
|
3248
|
+
re-point is
|
|
2931
3249
|
recorded on the client: the live base URL is observable via
|
|
2932
3250
|
`http.baseUrl` (the configured `config.baseUrl` stays the
|
|
2933
3251
|
construction-time value — the re-point is process-lifetime transport
|
package/docs/LIVE-TESTING.md
CHANGED
|
@@ -128,25 +128,25 @@ DRY-RUN-ONLY verdict (the only no-credentials path, exit 0).
|
|
|
128
128
|
|
|
129
129
|
npm run live-coverage -- --only tickets # one resource's ops
|
|
130
130
|
npm run live-coverage -- --only configuration_item_ssl # the cisan node-parity op
|
|
131
|
-
npm run live-coverage # FULL sweep:
|
|
131
|
+
npm run live-coverage # FULL sweep: 1281 rows, --max-calls 1600
|
|
132
132
|
|
|
133
133
|
Full-sweep budget (worst case, every table non-empty — the machine-readable
|
|
134
|
-
number is the report's `budget` block): PLAN-HONEST
|
|
134
|
+
number is the report's `budget` block): PLAN-HONEST 1016 bounded op calls
|
|
135
135
|
(every read/helper row = at most ONE bounded call — reads use
|
|
136
136
|
`queryBounded` limit 2, a TOTAL cap; the served-artifact rows —
|
|
137
137
|
operations.blindSurfaces (issue #11), operations.createGuidance (issue
|
|
138
138
|
#12) — plan the op call at ZERO wire, but still count as one op call in
|
|
139
|
-
this number: the budget counts ROW calls, not raw wire, so the
|
|
140
|
-
conservative row-based figure
|
|
141
|
-
+ <=
|
|
142
|
-
+ <=
|
|
143
|
-
the cache) = <=
|
|
139
|
+
this number: the budget counts ROW calls, not raw wire, so the 1016 is the
|
|
140
|
+
conservative row-based figure)
|
|
141
|
+
+ <=82 best-effort update diff GETs
|
|
142
|
+
+ <=129 cached limit-1 id reads (one per resource; the parent chain shares
|
|
143
|
+
the cache) = <= 1227. WIRE-HONEST: the dry-run verification reads ride on
|
|
144
144
|
top — 54 delete current-record GETs + 63 scoped-create parent-verify GETs
|
|
145
|
-
+ 20 getContext part GETs + <=
|
|
145
|
+
+ 20 getContext part GETs + <=18 special-op reads (incl. the issue #9
|
|
146
146
|
`operations.recovery` dry-run find — ONE bounded query, zero mutating
|
|
147
|
-
wire) = <=
|
|
148
|
-
--max-calls cap bounds the row-based plan (actualCalls <=
|
|
149
|
-
count cannot exceed plan + the stated verification set (and
|
|
147
|
+
wire) = <=155 — so the worst case is <= 1382 raw wire requests. The 1600
|
|
148
|
+
--max-calls cap bounds the row-based plan (actualCalls <= 1227); the wire
|
|
149
|
+
count cannot exceed plan + the stated verification set (and 1382 is far
|
|
150
150
|
under the API's 10k req/hour). ZERO mutating requests: every mutation travels
|
|
151
151
|
`dryRun: true` (the sweep flag + the SDK governor's dry-run-first default,
|
|
152
152
|
double-locked; pinned per-resource in the generated test suite), and EVERY
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-autotask",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
4
4
|
"description": "Fully-typed TypeScript SDK for the Autotask (Datto) PSA REST API. Zero runtime dependencies. Built for building MCP servers and integrations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|