@allisall/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -4
- package/dist/client.d.ts +81 -37
- package/dist/client.js +136 -73
- package/dist/generated/openapi.d.ts +7724 -0
- package/dist/generated/openapi.js +4 -0
- package/dist/generated/operations.d.ts +37 -0
- package/dist/generated/operations.js +976 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -0
- package/dist/ingest.d.ts +1 -1
- package/dist/ingest.js +2 -2
- package/dist/money.d.ts +57 -0
- package/dist/money.js +284 -0
- package/dist/multipart.d.ts +27 -0
- package/dist/multipart.js +48 -0
- package/dist/options.d.ts +50 -0
- package/dist/options.js +73 -0
- package/dist/pagination.d.ts +55 -0
- package/dist/pagination.js +85 -0
- package/dist/request.d.ts +117 -0
- package/dist/request.js +363 -0
- package/dist/resources/agents.d.ts +51 -0
- package/dist/resources/agents.js +65 -0
- package/dist/resources/analytics.d.ts +42 -0
- package/dist/resources/analytics.js +68 -0
- package/dist/resources/api-keys.d.ts +54 -0
- package/dist/resources/api-keys.js +63 -0
- package/dist/resources/calculations.d.ts +78 -0
- package/dist/resources/calculations.js +111 -0
- package/dist/resources/clients.d.ts +65 -0
- package/dist/resources/clients.js +85 -0
- package/dist/resources/cost.d.ts +69 -0
- package/dist/resources/cost.js +89 -0
- package/dist/resources/dashboard.d.ts +24 -0
- package/dist/resources/dashboard.js +28 -0
- package/dist/resources/events.d.ts +47 -0
- package/dist/resources/events.js +69 -0
- package/dist/resources/health.d.ts +14 -0
- package/dist/resources/health.js +17 -0
- package/dist/resources/imports.d.ts +33 -0
- package/dist/resources/imports.js +37 -0
- package/dist/resources/outcome-types.d.ts +25 -0
- package/dist/resources/outcome-types.js +33 -0
- package/dist/resources/reports.d.ts +77 -0
- package/dist/resources/reports.js +103 -0
- package/dist/resources/value-models.d.ts +92 -0
- package/dist/resources/value-models.js +126 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +2 -1
- package/dist/share.d.ts +52 -0
- package/dist/share.js +60 -0
- package/dist/transport.d.ts +54 -5
- package/dist/transport.js +81 -32
- package/dist/types.d.ts +135 -2
- package/dist/types.js +12 -3
- package/dist/unicode.d.ts +2 -0
- package/dist/unicode.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
export { Allisall, type ClientOptions
|
|
1
|
+
export { Allisall, type ClientOptions } from "./client.js";
|
|
2
2
|
export { type Decimal, decimal } from "./decimal.js";
|
|
3
3
|
export { AllisallError, ApiError, ConnectionError, IngestError, } from "./errors.js";
|
|
4
|
+
export type { MoneyInput } from "./money.js";
|
|
5
|
+
export type { ImportUpload } from "./multipart.js";
|
|
6
|
+
export type { IdempotentRequestOptions, RequestOptions } from "./options.js";
|
|
7
|
+
export type { Page, PagePromise } from "./pagination.js";
|
|
8
|
+
export { getSharedReport, type SharedReportOptions } from "./share.js";
|
|
4
9
|
export type { FetchLike, TokenProvider } from "./transport.js";
|
|
5
|
-
export type { ErrorCode, EventCategory, EventInput, FieldError, FieldErrorCode, IngestRowError, IngestSummary, Problem, } from "./types.js";
|
|
10
|
+
export type { Agent, AgentCostLine, AgentCreate, AgentDetail, AgentList, AgentListItem, AgentPatch, AgentStats, AgentStatus, Anomalies, ApiKey, ApiKeyCreate, ApiKeyList, ApiKeyWithToken, AssignedAgent, AttributedEvent, Calculation, CalculationCreate, CalculationList, CalculationSnapshot, CalculationState, CalculationSummary, Checklist, ChecklistInput, Client, ClientAgents, ClientAgentsSet, ClientCost, ClientCreate, ClientDetail, ClientEventList, ClientList, ClientPatch, ClientStatus, ClientValue, CostCoverage, CostLineItem, CostLineItemCreate, CostLineItemList, CostLineItemPatch, CostResult, CostTarget, CostTargetInput, CostTargetKind, Dashboard, Disclosure, DisclosureEdit, DisclosureKind, ErrorCode, EventCategory, EventInput, EventSource, FieldError, FieldErrorCode, GetAgentQuery, GetClientAnomaliesQuery, GetClientCostQuery, GetClientRoiQuery, GetClientValueQuery, GetDashboardQuery, Health, HealthDirection, HealthUnavailable, ImportMapping, ImportPreview, ImportResult, ImportRowError, IngestRowError, IngestSummary, ListAgentsQuery, ListApiKeysQuery, ListCalculationReportsQuery, ListClientCalculationsQuery, ListClientEventsQuery, ListClientOutcomeTypesQuery, ListClientsQuery, ListCostLineItemsQuery, ListOutcomeTypesQuery, ListRenewalsQuery, ListValueModelsQuery, ListValueTemplatesQuery, ManualCostLine, ModelRate, ModelRateInput, OutcomeTypeList, OutcomeTypeUsage, Period, PeriodInput, PortfolioClient, PortfolioExclusionReason, PreviewEvent, PreviewRow, PriceTable, PriceTableSet, Problem, PublicReport, Ratios, Renewal, RenewalList, Report, ReportList, ReportPatch, ReportSummary, ReportWithPreview, RoiResult, Scope, TokenCostLine, UnattributedLineItem, UndefinedReason, ValueCoverage, ValueLine, ValueLineItem, ValueLineItemInput, ValueLineItemsSet, ValueModel, ValueModelCreate, ValueModelList, ValueModelPatch, ValueModelRef, ValueModelStatus, ValueModelSummary, ValueResult, ValueTemplate, ValueTemplateLineItem, ValueTemplateList, } from "./types.js";
|
package/dist/index.js
CHANGED
package/dist/ingest.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { EventInput, IngestResult, IngestSummary } from "./types.js";
|
|
|
2
2
|
/** The most events one batch holds. */
|
|
3
3
|
export declare const MAX_BATCH_EVENTS = 500;
|
|
4
4
|
/** The API's body limit for a batch, in bytes, inclusive. */
|
|
5
|
-
export declare const MAX_BODY_BYTES =
|
|
5
|
+
export declare const MAX_BODY_BYTES = 4000000;
|
|
6
6
|
/** The largest event that fits in a batch alone. */
|
|
7
7
|
export declare const MAX_EVENT_BYTES: number;
|
|
8
8
|
/** Sends one batch body of `count` events and resolves with the API's outcome for it. */
|
package/dist/ingest.js
CHANGED
|
@@ -6,7 +6,7 @@ import { uuidv4 } from "./uuid.js";
|
|
|
6
6
|
/** The most events one batch holds. */
|
|
7
7
|
export const MAX_BATCH_EVENTS = 500;
|
|
8
8
|
/** The API's body limit for a batch, in bytes, inclusive. */
|
|
9
|
-
export const MAX_BODY_BYTES =
|
|
9
|
+
export const MAX_BODY_BYTES = 4_000_000;
|
|
10
10
|
/** The bytes of `{"events":[` and `]}` around a batch's events. */
|
|
11
11
|
const ENVELOPE_BYTES = 13;
|
|
12
12
|
/** The largest event that fits in a batch alone. */
|
|
@@ -106,7 +106,7 @@ function tooLarge(p) {
|
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
108
|
* Packs events into batches in input order. A batch closes at 500 events,
|
|
109
|
-
* or before an event that would take its body over
|
|
109
|
+
* or before an event that would take its body over 4,000,000 bytes. An
|
|
110
110
|
* event too large to send alone joins no batch and becomes a row error.
|
|
111
111
|
*/
|
|
112
112
|
function split(prepared) {
|
package/dist/money.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Decimal } from "./decimal.js";
|
|
2
|
+
import { type JsonPath } from "./generated/operations.js";
|
|
3
|
+
/**
|
|
4
|
+
* A request type with every `Decimal` in it widened to `Decimal | number`:
|
|
5
|
+
* what a request accepts where the API takes money. Use it to type a body
|
|
6
|
+
* you build before passing it to a method, such as
|
|
7
|
+
* `MoneyInput<CostLineItemCreate>`.
|
|
8
|
+
*
|
|
9
|
+
* A `Decimal`, such as one read from an earlier response, is sent with its
|
|
10
|
+
* own digits. A `number` is sent as its shortest round-trip text, so a
|
|
11
|
+
* literal such as `12.5` is sent exactly; a computed float such as
|
|
12
|
+
* `0.1 + 0.2` is sent as `0.30000000000000004`, which the API refuses as
|
|
13
|
+
* too precise rather than storing it inexactly. Build a `Decimal` with
|
|
14
|
+
* `decimal()` when the amount must be exact.
|
|
15
|
+
*/
|
|
16
|
+
export type MoneyInput<T> = T extends Decimal ? Decimal | number : T extends Date ? T : T extends (infer U)[] ? MoneyInput<U>[] : T extends object ? {
|
|
17
|
+
[K in keyof T]: MoneyInput<T[K]>;
|
|
18
|
+
} : T;
|
|
19
|
+
/**
|
|
20
|
+
* The largest exponent magnitude the decoder writes out in digits. Money is
|
|
21
|
+
* far smaller; the bound keeps a hostile exponent from building a huge
|
|
22
|
+
* string.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MAX_EXPONENT = 400;
|
|
25
|
+
/**
|
|
26
|
+
* The text of a `Decimal` equal to a JSON number's text. A number in
|
|
27
|
+
* exponent form, such as `1e-7`, is written out in digits; the conversion
|
|
28
|
+
* moves the decimal point and never rounds.
|
|
29
|
+
*
|
|
30
|
+
* @throws {AllisallError} when the exponent's magnitude exceeds
|
|
31
|
+
* `MAX_EXPONENT`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function decimalOfJsonNumber(text: string): Decimal | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Parses a response body. Every number at one of `moneyPaths` becomes a
|
|
36
|
+
* `Decimal` of the number's text; every other number becomes the `number`
|
|
37
|
+
* `JSON.parse` would give. A `null` stays `null`.
|
|
38
|
+
*
|
|
39
|
+
* @throws {SyntaxError} when `text` is not JSON.
|
|
40
|
+
* @throws {AllisallError} when the body holds money and the runtime does not
|
|
41
|
+
* give a reviver a number's text.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decodeJson(text: string, moneyPaths: readonly JsonPath[]): unknown;
|
|
44
|
+
/**
|
|
45
|
+
* Serialises a request body. At each of `moneyPaths`, a `Decimal` is
|
|
46
|
+
* written as its own digits and a `number` as its shortest round-trip text.
|
|
47
|
+
* The caller's object is not changed. Every member it holds is written, as
|
|
48
|
+
* `JSON.stringify` writes it.
|
|
49
|
+
*
|
|
50
|
+
* @throws {TypeError} when a value at a money path is not a `Decimal`, a
|
|
51
|
+
* finite number or `null`; when any value is an invalid `Date`, a
|
|
52
|
+
* non-finite number, or text holding a lone surrogate, naming its path; or
|
|
53
|
+
* when the body has no JSON text, such as `undefined`.
|
|
54
|
+
* @throws {AllisallError} when the body holds a `Decimal` and the runtime has
|
|
55
|
+
* no `JSON.rawJSON`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function encodeJson(body: unknown, moneyPaths: readonly JsonPath[]): string;
|
package/dist/money.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Exact money in JSON bodies. A `Money` value is a JSON number on the wire.
|
|
2
|
+
// The SDK never lets a binary float hold one: a response's money is decoded
|
|
3
|
+
// from the number's own text into a `Decimal`, and a request's `Decimal` is
|
|
4
|
+
// written as its own text.
|
|
5
|
+
//
|
|
6
|
+
// Both directions work from an operation's `Money` paths, from the operation
|
|
7
|
+
// table. A path, not a member's name, decides whether a number is money:
|
|
8
|
+
// `total` is money in one schema and a count in another.
|
|
9
|
+
//
|
|
10
|
+
// Two platform features make this possible, and TypeScript's own library
|
|
11
|
+
// files declare neither, so this module declares the shapes it reads:
|
|
12
|
+
//
|
|
13
|
+
// - The third argument a `JSON.parse` reviver receives, whose `source` is a
|
|
14
|
+
// number's text as it appeared in the JSON.
|
|
15
|
+
// - `JSON.rawJSON(text)`, whose result `JSON.stringify` writes as `text`,
|
|
16
|
+
// unquoted.
|
|
17
|
+
//
|
|
18
|
+
// Both arrived in Chrome 114, Firefox 135, Safari 18.4 and Node 21. Without
|
|
19
|
+
// them, money cannot be handled exactly, and the SDK refuses rather than
|
|
20
|
+
// fall back on a float.
|
|
21
|
+
import { isDecimalText } from "./decimal.js";
|
|
22
|
+
import { AllisallError } from "./errors.js";
|
|
23
|
+
import { ITEMS } from "./generated/operations.js";
|
|
24
|
+
import { hasLoneSurrogate } from "./unicode.js";
|
|
25
|
+
const trees = new WeakMap();
|
|
26
|
+
function treeOf(paths) {
|
|
27
|
+
const cached = trees.get(paths);
|
|
28
|
+
if (cached !== undefined)
|
|
29
|
+
return cached;
|
|
30
|
+
const root = { money: false, children: new Map() };
|
|
31
|
+
for (const path of paths) {
|
|
32
|
+
let node = root;
|
|
33
|
+
for (const segment of path) {
|
|
34
|
+
let child = node.children.get(segment);
|
|
35
|
+
if (child === undefined) {
|
|
36
|
+
child = { money: false, children: new Map() };
|
|
37
|
+
node.children.set(segment, child);
|
|
38
|
+
}
|
|
39
|
+
node = child;
|
|
40
|
+
}
|
|
41
|
+
node.money = true;
|
|
42
|
+
}
|
|
43
|
+
trees.set(paths, root);
|
|
44
|
+
return root;
|
|
45
|
+
}
|
|
46
|
+
function childOf(node, segment) {
|
|
47
|
+
return node?.children.get(segment);
|
|
48
|
+
}
|
|
49
|
+
function isPlainObject(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null)
|
|
51
|
+
return false;
|
|
52
|
+
const proto = Object.getPrototypeOf(value);
|
|
53
|
+
return proto === Object.prototype || proto === null;
|
|
54
|
+
}
|
|
55
|
+
// ---- Decoding a response ----
|
|
56
|
+
/**
|
|
57
|
+
* A JSON number as the first pass of decoding leaves it: its value, and its
|
|
58
|
+
* text when the runtime supplied it.
|
|
59
|
+
*/
|
|
60
|
+
class JsonNumber {
|
|
61
|
+
value;
|
|
62
|
+
source;
|
|
63
|
+
constructor(value, source) {
|
|
64
|
+
this.value = value;
|
|
65
|
+
this.source = source;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The text of the number a reviver was called with, when the runtime supplies it. */
|
|
69
|
+
function sourceText(context) {
|
|
70
|
+
if (typeof context !== "object" || context === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
const source = Reflect.get(context, "source");
|
|
73
|
+
return typeof source === "string" ? source : undefined;
|
|
74
|
+
}
|
|
75
|
+
function keepNumberText(_key, value, context) {
|
|
76
|
+
return typeof value === "number"
|
|
77
|
+
? new JsonNumber(value, sourceText(context))
|
|
78
|
+
: value;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The largest exponent magnitude the decoder writes out in digits. Money is
|
|
82
|
+
* far smaller; the bound keeps a hostile exponent from building a huge
|
|
83
|
+
* string.
|
|
84
|
+
*/
|
|
85
|
+
export const MAX_EXPONENT = 400;
|
|
86
|
+
const JSON_NUMBER = /^(-?)(0|[1-9][0-9]*)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/;
|
|
87
|
+
/**
|
|
88
|
+
* The text of a `Decimal` equal to a JSON number's text. A number in
|
|
89
|
+
* exponent form, such as `1e-7`, is written out in digits; the conversion
|
|
90
|
+
* moves the decimal point and never rounds.
|
|
91
|
+
*
|
|
92
|
+
* @throws {AllisallError} when the exponent's magnitude exceeds
|
|
93
|
+
* `MAX_EXPONENT`.
|
|
94
|
+
*/
|
|
95
|
+
export function decimalOfJsonNumber(text) {
|
|
96
|
+
if (isDecimalText(text)) {
|
|
97
|
+
// The syntax check is the invariant the brand stands for.
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
100
|
+
const match = JSON_NUMBER.exec(text);
|
|
101
|
+
if (match === null)
|
|
102
|
+
return undefined;
|
|
103
|
+
const [, sign = "", whole = "", fraction = "", exponent = "0"] = match;
|
|
104
|
+
const digits = whole + fraction;
|
|
105
|
+
const shift = Number(exponent);
|
|
106
|
+
if (!(Math.abs(shift) <= MAX_EXPONENT)) {
|
|
107
|
+
throw new AllisallError(`allisall: the API sent money as ${JSON.stringify(text)}, whose exponent is beyond ±${MAX_EXPONENT}`);
|
|
108
|
+
}
|
|
109
|
+
const point = whole.length + shift;
|
|
110
|
+
let out;
|
|
111
|
+
if (point <= 0)
|
|
112
|
+
out = `0.${"0".repeat(-point)}${digits}`;
|
|
113
|
+
else if (point >= digits.length)
|
|
114
|
+
out = digits + "0".repeat(point - digits.length);
|
|
115
|
+
else
|
|
116
|
+
out = `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
117
|
+
// Moving the point can leave leading zeros in the whole part, as `0.5e1`
|
|
118
|
+
// becomes `05`.
|
|
119
|
+
out = out.replace(/^0+(?=[0-9])/, "");
|
|
120
|
+
const result = sign + out;
|
|
121
|
+
// Every branch above writes decimal text; the check keeps the brand honest.
|
|
122
|
+
return isDecimalText(result) ? result : undefined;
|
|
123
|
+
}
|
|
124
|
+
function place(value, node) {
|
|
125
|
+
if (value instanceof JsonNumber) {
|
|
126
|
+
if (node?.money !== true)
|
|
127
|
+
return value.value;
|
|
128
|
+
if (value.source === undefined) {
|
|
129
|
+
throw new AllisallError("allisall: this runtime does not give a JSON.parse reviver the source text of a number (the reviver's context argument), so the money in this response cannot be read exactly; use Node 21 or later, Chrome 114, Firefox 135 or Safari 18.4 or later");
|
|
130
|
+
}
|
|
131
|
+
const amount = decimalOfJsonNumber(value.source);
|
|
132
|
+
if (amount === undefined) {
|
|
133
|
+
throw new AllisallError(`allisall: the API sent money as ${JSON.stringify(value.source)}, which is not a decimal number`);
|
|
134
|
+
}
|
|
135
|
+
return amount;
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
const items = childOf(node, ITEMS);
|
|
139
|
+
for (let i = 0; i < value.length; i++)
|
|
140
|
+
value[i] = place(value[i], items);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
if (typeof value === "object" && value !== null) {
|
|
144
|
+
for (const key of Object.keys(value)) {
|
|
145
|
+
Reflect.set(value, key, place(Reflect.get(value, key), childOf(node, key)));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parses a response body. Every number at one of `moneyPaths` becomes a
|
|
152
|
+
* `Decimal` of the number's text; every other number becomes the `number`
|
|
153
|
+
* `JSON.parse` would give. A `null` stays `null`.
|
|
154
|
+
*
|
|
155
|
+
* @throws {SyntaxError} when `text` is not JSON.
|
|
156
|
+
* @throws {AllisallError} when the body holds money and the runtime does not
|
|
157
|
+
* give a reviver a number's text.
|
|
158
|
+
*/
|
|
159
|
+
export function decodeJson(text, moneyPaths) {
|
|
160
|
+
const parsed = JSON.parse(text, keepNumberText);
|
|
161
|
+
return place(parsed, treeOf(moneyPaths));
|
|
162
|
+
}
|
|
163
|
+
// ---- Encoding a request ----
|
|
164
|
+
/** `JSON.rawJSON`, when the runtime has it. */
|
|
165
|
+
function rawJson() {
|
|
166
|
+
const fn = Reflect.get(JSON, "rawJSON");
|
|
167
|
+
if (typeof fn !== "function")
|
|
168
|
+
return undefined;
|
|
169
|
+
return (text) => Reflect.apply(fn, JSON, [text]);
|
|
170
|
+
}
|
|
171
|
+
function kindOf(value) {
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
return "an array";
|
|
174
|
+
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
175
|
+
}
|
|
176
|
+
function encodeMoney(value, path) {
|
|
177
|
+
const where = path.length === 0 ? "the body" : `/${path.join("/")}`;
|
|
178
|
+
if (typeof value === "number") {
|
|
179
|
+
if (!Number.isFinite(value)) {
|
|
180
|
+
throw new TypeError(`allisall: the money at ${where} is ${value}; it must be a finite number`);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
// A money member may be nullable; an absent or undefined one is not sent.
|
|
185
|
+
if (value === null || value === undefined)
|
|
186
|
+
return value;
|
|
187
|
+
if (typeof value !== "string") {
|
|
188
|
+
throw new TypeError(`allisall: the money at ${where} is ${kindOf(value)}; it must be a Decimal, a finite number or null`);
|
|
189
|
+
}
|
|
190
|
+
if (!isDecimalText(value)) {
|
|
191
|
+
throw new TypeError(`allisall: the money at ${where} is ${JSON.stringify(value)}, which is not a decimal; build it with decimal()`);
|
|
192
|
+
}
|
|
193
|
+
const raw = rawJson();
|
|
194
|
+
if (raw === undefined) {
|
|
195
|
+
throw new AllisallError("allisall: this runtime has no JSON.rawJSON, so a Decimal cannot be sent exactly; use Node 21 or later, Chrome 114, Firefox 135 or Safari 18.4 or later");
|
|
196
|
+
}
|
|
197
|
+
return raw(value);
|
|
198
|
+
}
|
|
199
|
+
function copyAlong(value, node, path) {
|
|
200
|
+
if (node.money)
|
|
201
|
+
return encodeMoney(value, path);
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
const items = node.children.get(ITEMS);
|
|
204
|
+
if (items === undefined)
|
|
205
|
+
return value;
|
|
206
|
+
return value.map((item, i) => copyAlong(item, items, [...path, String(i)]));
|
|
207
|
+
}
|
|
208
|
+
if (isPlainObject(value)) {
|
|
209
|
+
const copy = {};
|
|
210
|
+
for (const [key, member] of Object.entries(value)) {
|
|
211
|
+
const child = node.children.get(key);
|
|
212
|
+
const written = child === undefined ? member : copyAlong(member, child, [...path, key]);
|
|
213
|
+
Object.defineProperty(copy, key, {
|
|
214
|
+
value: written,
|
|
215
|
+
enumerable: true,
|
|
216
|
+
writable: true,
|
|
217
|
+
configurable: true,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return copy;
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* A `JSON.stringify` replacer refusing the values it would write as `null`
|
|
226
|
+
* in silence: an invalid `Date`, and a non-finite number. On a merge patch,
|
|
227
|
+
* `null` clears a member, so either would change data the caller never
|
|
228
|
+
* meant to touch. It also refuses text holding a lone surrogate, in a
|
|
229
|
+
* string or a member name, which the API would store as U+FFFD.
|
|
230
|
+
*
|
|
231
|
+
* A replacer sees a member's key and its holder, not its path, so each
|
|
232
|
+
* object's path is recorded as it is reached, before its members are.
|
|
233
|
+
*/
|
|
234
|
+
function refuseLostValues() {
|
|
235
|
+
const paths = new WeakMap();
|
|
236
|
+
return function (key, value) {
|
|
237
|
+
const holder = typeof this === "object" && this !== null ? this : undefined;
|
|
238
|
+
const parent = holder === undefined ? undefined : paths.get(holder);
|
|
239
|
+
// The root's holder is the wrapper JSON.stringify makes, never recorded.
|
|
240
|
+
// A name is checked before it joins a path, so no message carries it.
|
|
241
|
+
if (parent !== undefined && hasLoneSurrogate(key)) {
|
|
242
|
+
throw new TypeError(`allisall: a member name in the object at ${parent === "" ? "the body" : parent} is not valid Unicode (a lone surrogate)`);
|
|
243
|
+
}
|
|
244
|
+
const path = parent === undefined ? "" : `${parent}/${key}`;
|
|
245
|
+
const where = path === "" ? "the body" : path;
|
|
246
|
+
// The value has been through toJSON already; the holder keeps the Date.
|
|
247
|
+
const original = holder === undefined ? value : Reflect.get(holder, key);
|
|
248
|
+
if (original instanceof Date && Number.isNaN(original.getTime())) {
|
|
249
|
+
throw new TypeError(`allisall: the value at ${where} is an invalid Date`);
|
|
250
|
+
}
|
|
251
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
252
|
+
throw new TypeError(`allisall: the value at ${where} is ${value}; it must be a finite number`);
|
|
253
|
+
}
|
|
254
|
+
if (typeof value === "string" && hasLoneSurrogate(value)) {
|
|
255
|
+
throw new TypeError(`allisall: the value at ${where} is not valid Unicode (a lone surrogate)`);
|
|
256
|
+
}
|
|
257
|
+
if (typeof value === "object" && value !== null)
|
|
258
|
+
paths.set(value, path);
|
|
259
|
+
return value;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Serialises a request body. At each of `moneyPaths`, a `Decimal` is
|
|
264
|
+
* written as its own digits and a `number` as its shortest round-trip text.
|
|
265
|
+
* The caller's object is not changed. Every member it holds is written, as
|
|
266
|
+
* `JSON.stringify` writes it.
|
|
267
|
+
*
|
|
268
|
+
* @throws {TypeError} when a value at a money path is not a `Decimal`, a
|
|
269
|
+
* finite number or `null`; when any value is an invalid `Date`, a
|
|
270
|
+
* non-finite number, or text holding a lone surrogate, naming its path; or
|
|
271
|
+
* when the body has no JSON text, such as `undefined`.
|
|
272
|
+
* @throws {AllisallError} when the body holds a `Decimal` and the runtime has
|
|
273
|
+
* no `JSON.rawJSON`.
|
|
274
|
+
*/
|
|
275
|
+
export function encodeJson(body, moneyPaths) {
|
|
276
|
+
const copy = copyAlong(body, treeOf(moneyPaths), []);
|
|
277
|
+
// JSON.stringify gives undefined, despite its declared type, for a body
|
|
278
|
+
// with no JSON text: undefined, a function or a symbol.
|
|
279
|
+
const text = JSON.stringify(copy, refuseLostValues());
|
|
280
|
+
if (typeof text !== "string") {
|
|
281
|
+
throw new TypeError(`allisall: a request body of type ${typeof body} has no JSON text`);
|
|
282
|
+
}
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { components } from "./generated/openapi.js";
|
|
2
|
+
/** A CSV upload: the mapping of its columns, and the file itself. */
|
|
3
|
+
export interface ImportUpload {
|
|
4
|
+
/** How the file's columns map to event members, sent as the first part. */
|
|
5
|
+
mapping: components["schemas"]["ImportMapping"];
|
|
6
|
+
/**
|
|
7
|
+
* The CSV file, sent as the second part with the type `text/csv`. A
|
|
8
|
+
* `Blob` is sent without being read into memory first; in Node,
|
|
9
|
+
* `fs.openAsBlob(path)` gives one for a file on disk. A stream is not
|
|
10
|
+
* accepted, because a retry must send the same bytes again.
|
|
11
|
+
*/
|
|
12
|
+
file: Blob | Uint8Array | string;
|
|
13
|
+
/**
|
|
14
|
+
* The file name the import records. Defaults to the name of a `File`, and
|
|
15
|
+
* otherwise to `upload.csv`; an empty name counts as none.
|
|
16
|
+
*/
|
|
17
|
+
filename?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds the form once, `mapping` before `file`, as the API reads them. The
|
|
21
|
+
* same form is the body of every attempt.
|
|
22
|
+
*
|
|
23
|
+
* @throws {TypeError} when the upload is not an object, the mapping has no
|
|
24
|
+
* JSON text, or the file is not a `Blob`, a `Uint8Array` or a string, or
|
|
25
|
+
* is a string holding a lone surrogate.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formOf(upload: ImportUpload): FormData;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A CSV import's body: a form of two parts, the mapping and then the file.
|
|
2
|
+
import { encodeJson } from "./money.js";
|
|
3
|
+
import { hasLoneSurrogate } from "./unicode.js";
|
|
4
|
+
const DEFAULT_FILENAME = "upload.csv";
|
|
5
|
+
function nameOf(file) {
|
|
6
|
+
const name = Reflect.get(file, "name");
|
|
7
|
+
return typeof name === "string" && name !== "" ? name : undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds the form once, `mapping` before `file`, as the API reads them. The
|
|
11
|
+
* same form is the body of every attempt.
|
|
12
|
+
*
|
|
13
|
+
* @throws {TypeError} when the upload is not an object, the mapping has no
|
|
14
|
+
* JSON text, or the file is not a `Blob`, a `Uint8Array` or a string, or
|
|
15
|
+
* is a string holding a lone surrogate.
|
|
16
|
+
*/
|
|
17
|
+
export function formOf(upload) {
|
|
18
|
+
if (typeof upload !== "object" || upload === null) {
|
|
19
|
+
throw new TypeError("allisall: an import takes an object with mapping and file");
|
|
20
|
+
}
|
|
21
|
+
const { mapping, file, filename } = upload;
|
|
22
|
+
const mappingPart = new Blob([encodeJson(mapping, [])], {
|
|
23
|
+
type: "application/json",
|
|
24
|
+
});
|
|
25
|
+
let filePart;
|
|
26
|
+
let defaultName = DEFAULT_FILENAME;
|
|
27
|
+
if (file instanceof Blob) {
|
|
28
|
+
// A File keeps its own name; its type is set to the one the API reads.
|
|
29
|
+
defaultName = nameOf(file) ?? DEFAULT_FILENAME;
|
|
30
|
+
filePart = new Blob([file], { type: "text/csv" });
|
|
31
|
+
}
|
|
32
|
+
else if (file instanceof Uint8Array || typeof file === "string") {
|
|
33
|
+
// A Blob writes a lone surrogate as U+FFFD, changing the file in silence.
|
|
34
|
+
if (typeof file === "string" && hasLoneSurrogate(file)) {
|
|
35
|
+
throw new TypeError("allisall: the import's file is not valid Unicode (a lone surrogate)");
|
|
36
|
+
}
|
|
37
|
+
filePart = new Blob([file], { type: "text/csv" });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new TypeError("allisall: an import's file must be a Blob, a Uint8Array or a string; a stream cannot be sent again on a retry");
|
|
41
|
+
}
|
|
42
|
+
const form = new FormData();
|
|
43
|
+
form.append("mapping", mappingPart, "mapping.json");
|
|
44
|
+
// An empty name falls back, as a File with no name does.
|
|
45
|
+
const name = typeof filename === "string" && filename !== "" ? filename : defaultName;
|
|
46
|
+
form.append("file", filePart, name);
|
|
47
|
+
return form;
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** The options of one call. Each member left out takes the client's. */
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
/** Cancels the call, including any wait between retries. */
|
|
4
|
+
signal?: AbortSignal | undefined;
|
|
5
|
+
/** The timeout of each attempt of this call, in milliseconds. */
|
|
6
|
+
timeout?: number | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* How many times this call is retried. It cannot make a request eligible
|
|
9
|
+
* that the SDK never retries, such as a `PATCH`.
|
|
10
|
+
*/
|
|
11
|
+
maxRetries?: number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Extra headers for this call, sent over the client's `defaultHeaders`.
|
|
14
|
+
* They may not name `Authorization`, `Content-Type`, `Idempotency-Key` or
|
|
15
|
+
* `X-Request-Id`, which the SDK sets. In a browser, a header must be one
|
|
16
|
+
* the API's CORS policy allows.
|
|
17
|
+
*/
|
|
18
|
+
headers?: Readonly<Record<string, string>> | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** The options of a call that creates something and takes an `Idempotency-Key`. */
|
|
21
|
+
export interface IdempotentRequestOptions extends RequestOptions {
|
|
22
|
+
/**
|
|
23
|
+
* The `Idempotency-Key` to send, as given. By default the SDK generates
|
|
24
|
+
* one per call and sends it on every attempt of that call. Set your own
|
|
25
|
+
* to make a create safe to repeat across process restarts.
|
|
26
|
+
*
|
|
27
|
+
* A second call with the same key and the same request, within 24 hours,
|
|
28
|
+
* gets the first call's stored response. Only a `2xx` is stored, so a key
|
|
29
|
+
* whose first call failed can be sent again. The same key with a
|
|
30
|
+
* different body is refused with `422` `idempotency_key_reused`.
|
|
31
|
+
*
|
|
32
|
+
* The key must be a string the platform can send as a header value: a
|
|
33
|
+
* newline, or a character above U+00FF, is a `TypeError` before sending.
|
|
34
|
+
*/
|
|
35
|
+
idempotencyKey?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
38
|
+
export declare const DEFAULT_MAX_RETRIES = 2;
|
|
39
|
+
export declare function resolveTimeout(option: number | undefined, fallback: number): number;
|
|
40
|
+
export declare function resolveMaxRetries(option: number | undefined, fallback: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Checks caller headers and returns a copy. `option` names the option the
|
|
43
|
+
* headers came from, for the message.
|
|
44
|
+
*
|
|
45
|
+
* @throws {TypeError} when the headers are not a plain object, or a header
|
|
46
|
+
* is one the SDK sets or is not a valid header name and string value.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveHeaders(headers: Readonly<Record<string, string>> | undefined, option: string): Readonly<Record<string, string>>;
|
|
49
|
+
/** Sets each of `extra` in `target`, replacing a header of the same name in any case. */
|
|
50
|
+
export declare function mergeHeaders(target: Record<string, string>, extra: Readonly<Record<string, string>>): void;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The options of one call, and how they are checked and combined with the
|
|
2
|
+
// client's.
|
|
3
|
+
import { AllisallError } from "./errors.js";
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 60_000;
|
|
5
|
+
export const DEFAULT_MAX_RETRIES = 2;
|
|
6
|
+
/** The headers the SDK sets itself, in lower case. */
|
|
7
|
+
const OWNED_HEADERS = new Set([
|
|
8
|
+
"authorization",
|
|
9
|
+
"content-type",
|
|
10
|
+
"idempotency-key",
|
|
11
|
+
"x-request-id",
|
|
12
|
+
]);
|
|
13
|
+
export function resolveTimeout(option, fallback) {
|
|
14
|
+
const timeout = option ?? fallback;
|
|
15
|
+
if (typeof timeout !== "number" ||
|
|
16
|
+
!Number.isFinite(timeout) ||
|
|
17
|
+
timeout <= 0) {
|
|
18
|
+
throw new AllisallError(`allisall: timeout must be a positive number of milliseconds, not ${String(timeout)}`);
|
|
19
|
+
}
|
|
20
|
+
return timeout;
|
|
21
|
+
}
|
|
22
|
+
export function resolveMaxRetries(option, fallback) {
|
|
23
|
+
const maxRetries = option ?? fallback;
|
|
24
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0) {
|
|
25
|
+
throw new AllisallError(`allisall: maxRetries must be a whole number, 0 or more, not ${String(maxRetries)}`);
|
|
26
|
+
}
|
|
27
|
+
return maxRetries;
|
|
28
|
+
}
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
if (typeof value !== "object" || value === null)
|
|
31
|
+
return false;
|
|
32
|
+
const proto = Object.getPrototypeOf(value);
|
|
33
|
+
return proto === Object.prototype || proto === null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Checks caller headers and returns a copy. `option` names the option the
|
|
37
|
+
* headers came from, for the message.
|
|
38
|
+
*
|
|
39
|
+
* @throws {TypeError} when the headers are not a plain object, or a header
|
|
40
|
+
* is one the SDK sets or is not a valid header name and string value.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveHeaders(headers, option) {
|
|
43
|
+
if (headers === undefined)
|
|
44
|
+
return {};
|
|
45
|
+
if (!isPlainObject(headers)) {
|
|
46
|
+
throw new TypeError(`allisall: ${option} must be a plain object of header names and string values`);
|
|
47
|
+
}
|
|
48
|
+
const copy = {};
|
|
49
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
50
|
+
if (OWNED_HEADERS.has(name.toLowerCase())) {
|
|
51
|
+
throw new TypeError(`allisall: ${option} may not set ${name}; the SDK sets it`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== "string") {
|
|
54
|
+
throw new TypeError(`allisall: ${option} sets ${name} to ${typeof value}; a header value must be a string`);
|
|
55
|
+
}
|
|
56
|
+
copy[name] = value;
|
|
57
|
+
}
|
|
58
|
+
// The platform's own check of names and values, so a malformed header is
|
|
59
|
+
// refused here rather than failing every attempt as a connection error.
|
|
60
|
+
new Headers(copy);
|
|
61
|
+
return copy;
|
|
62
|
+
}
|
|
63
|
+
/** Sets each of `extra` in `target`, replacing a header of the same name in any case. */
|
|
64
|
+
export function mergeHeaders(target, extra) {
|
|
65
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
66
|
+
const lower = name.toLowerCase();
|
|
67
|
+
for (const existing of Object.keys(target)) {
|
|
68
|
+
if (existing.toLowerCase() === lower)
|
|
69
|
+
delete target[existing];
|
|
70
|
+
}
|
|
71
|
+
target[name] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { RequestOptions } from "./options.js";
|
|
2
|
+
/** The envelope every list response shares. */
|
|
3
|
+
export interface ListBody {
|
|
4
|
+
readonly data: readonly unknown[];
|
|
5
|
+
readonly has_more: boolean;
|
|
6
|
+
readonly next_cursor: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** Fetches the page after this one; the options apply to that request alone. */
|
|
9
|
+
type NextPage<Item, Body extends ListBody> = (options: RequestOptions | undefined) => Promise<Page<Item, Body>>;
|
|
10
|
+
/**
|
|
11
|
+
* One page of a list. `data` holds its items and `body` the whole response,
|
|
12
|
+
* such as the event drill-down's `total`. Iterate it with `for await` to
|
|
13
|
+
* walk its items and then every following page's.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Page<Item, Body extends ListBody> implements AsyncIterable<Item> {
|
|
16
|
+
#private;
|
|
17
|
+
/** The page's items. */
|
|
18
|
+
readonly data: Item[];
|
|
19
|
+
/** The whole decoded response. */
|
|
20
|
+
readonly body: Body;
|
|
21
|
+
/** Pages are made by list methods; you do not construct one. */
|
|
22
|
+
constructor(body: Body, next: NextPage<Item, Body> | undefined);
|
|
23
|
+
/** Whether another page follows this one: the response's `has_more`. */
|
|
24
|
+
hasNextPage(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Fetches the next page, with the same query and options as this one.
|
|
27
|
+
* `options` apply to this request alone, over the call's.
|
|
28
|
+
*
|
|
29
|
+
* @throws {AllisallError} when this is the last page.
|
|
30
|
+
*/
|
|
31
|
+
getNextPage(options?: RequestOptions): Promise<Page<Item, Body>>;
|
|
32
|
+
/** Yields this page's items, then each following page's, fetching each page as it is reached. */
|
|
33
|
+
[Symbol.asyncIterator](): AsyncIterator<Item>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The result of a list method. Await it for the first `Page`; iterate it
|
|
37
|
+
* with `for await` for every item of every page.
|
|
38
|
+
*
|
|
39
|
+
* Nothing is sent until it is awaited or iterated, so a list call whose
|
|
40
|
+
* result is never used sends no request.
|
|
41
|
+
*/
|
|
42
|
+
export declare class PagePromise<Item, Body extends ListBody> implements PromiseLike<Page<Item, Body>>, AsyncIterable<Item> {
|
|
43
|
+
#private;
|
|
44
|
+
/** Made by list methods; you do not construct one. */
|
|
45
|
+
constructor(load: () => Promise<Page<Item, Body>>);
|
|
46
|
+
/** Resolves with the first page, sending its request on first use. */
|
|
47
|
+
then<R1 = Page<Item, Body>, R2 = never>(onfulfilled?: ((page: Page<Item, Body>) => R1 | PromiseLike<R1>) | null | undefined, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null | undefined): Promise<R1 | R2>;
|
|
48
|
+
/** As `Promise.prototype.catch`, on the first page. */
|
|
49
|
+
catch<R = never>(onrejected?: ((reason: unknown) => R | PromiseLike<R>) | null | undefined): Promise<Page<Item, Body> | R>;
|
|
50
|
+
/** As `Promise.prototype.finally`, on the first page. */
|
|
51
|
+
finally(onfinally?: (() => void) | null | undefined): Promise<Page<Item, Body>>;
|
|
52
|
+
/** Yields every item of every page, from the first. */
|
|
53
|
+
[Symbol.asyncIterator](): AsyncIterator<Item>;
|
|
54
|
+
}
|
|
55
|
+
export {};
|