@elmohq/sdk 0.0.0-stage → 0.1.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/LICENSE.md +21 -0
- package/README.md +326 -2
- package/dist/calls/index.d.mts +288 -0
- package/dist/calls/index.d.mts.map +1 -0
- package/dist/calls/index.mjs +390 -0
- package/dist/calls/index.mjs.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1031 -0
- package/dist/index.mjs.map +1 -0
- package/dist/request-options-B9S9d24M.d.mts +1549 -0
- package/dist/request-options-B9S9d24M.d.mts.map +1 -0
- package/dist/tools-7Hs5J2xb.d.mts +1945 -0
- package/dist/tools-7Hs5J2xb.d.mts.map +1 -0
- package/dist/tools-B1lVtYAr.mjs +2459 -0
- package/dist/tools-B1lVtYAr.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -0
- package/dist/types/index.mjs +1 -0
- package/package.json +68 -4
- package/src/calls/brands-analytics-get.ts +33 -0
- package/src/calls/brands-citations-domains-list.ts +36 -0
- package/src/calls/brands-citations-urls-list.ts +32 -0
- package/src/calls/brands-create.ts +19 -0
- package/src/calls/brands-get.ts +19 -0
- package/src/calls/brands-list.ts +18 -0
- package/src/calls/brands-opportunities-get.ts +42 -0
- package/src/calls/brands-prompt-performance-list.ts +39 -0
- package/src/calls/brands-query-fanout-get.ts +30 -0
- package/src/calls/brands-tags-list.ts +28 -0
- package/src/calls/brands-update.ts +20 -0
- package/src/calls/competitors-create.ts +23 -0
- package/src/calls/competitors-delete.ts +19 -0
- package/src/calls/competitors-get.ts +19 -0
- package/src/calls/competitors-list.ts +23 -0
- package/src/calls/competitors-update.ts +24 -0
- package/src/calls/index.ts +33 -0
- package/src/calls/me-get.ts +18 -0
- package/src/calls/models-list.ts +20 -0
- package/src/calls/organizations-billing-get.ts +37 -0
- package/src/calls/organizations-get.ts +24 -0
- package/src/calls/organizations-list.ts +27 -0
- package/src/calls/prompts-create.ts +27 -0
- package/src/calls/prompts-delete.ts +28 -0
- package/src/calls/prompts-get.ts +23 -0
- package/src/calls/prompts-list.ts +28 -0
- package/src/calls/prompts-runs-get.ts +27 -0
- package/src/calls/prompts-runs-list.ts +30 -0
- package/src/calls/prompts-snapshot-get.ts +28 -0
- package/src/calls/prompts-update.ts +29 -0
- package/src/calls/reports-create.ts +31 -0
- package/src/calls/reports-get.ts +27 -0
- package/src/calls/reports-list.ts +28 -0
- package/src/calls/tools-analyze.ts +26 -0
- package/src/client.ts +649 -0
- package/src/index.ts +21 -0
- package/src/internal/binding/rest/binding.ts +127 -0
- package/src/internal/binding/rest/errors.ts +107 -0
- package/src/internal/binding/rest/path.ts +9 -0
- package/src/internal/binding/rest/url.ts +97 -0
- package/src/internal/codec/json.ts +66 -0
- package/src/internal/codec/registry.ts +55 -0
- package/src/internal/core/backoff.ts +170 -0
- package/src/internal/core/body.ts +54 -0
- package/src/internal/core/browser.ts +11 -0
- package/src/internal/core/call-promise.ts +145 -0
- package/src/internal/core/callable.ts +11 -0
- package/src/internal/core/client.ts +89 -0
- package/src/internal/core/config.ts +84 -0
- package/src/internal/core/credentials.ts +40 -0
- package/src/internal/core/dispatch.ts +225 -0
- package/src/internal/core/env.ts +68 -0
- package/src/internal/core/errors.ts +431 -0
- package/src/internal/core/features.ts +60 -0
- package/src/internal/core/lazy-promise.ts +39 -0
- package/src/internal/core/metadata.ts +79 -0
- package/src/internal/core/page.ts +402 -0
- package/src/internal/core/params.ts +98 -0
- package/src/internal/core/protocol.ts +17 -0
- package/src/internal/core/read-reply.ts +7 -0
- package/src/internal/core/text.ts +7 -0
- package/src/internal/core/types.ts +554 -0
- package/src/internal/feature/auth.ts +201 -0
- package/src/internal/feature/interceptors.ts +36 -0
- package/src/internal/feature/logger.ts +222 -0
- package/src/internal/feature/retry.ts +168 -0
- package/src/internal/feature/rules.ts +54 -0
- package/src/internal/feature/timeout.ts +146 -0
- package/src/internal/feature/validate.ts +82 -0
- package/src/internal/transport/fetch.ts +82 -0
- package/src/resources/analytics.ts +364 -0
- package/src/resources/brands.ts +288 -0
- package/src/resources/competitors.ts +213 -0
- package/src/resources/identity.ts +49 -0
- package/src/resources/models.ts +32 -0
- package/src/resources/opportunities.ts +81 -0
- package/src/resources/organizations.ts +182 -0
- package/src/resources/prompts.ts +293 -0
- package/src/resources/reports.ts +212 -0
- package/src/resources/runs.ts +124 -0
- package/src/resources/shared/auth.ts +13 -0
- package/src/resources/shared/request-options.ts +26 -0
- package/src/resources/shared/resource.ts +23 -0
- package/src/resources/shared/rule.ts +25 -0
- package/src/resources/snapshots.ts +54 -0
- package/src/resources/tags.ts +46 -0
- package/src/resources/tools.ts +69 -0
- package/src/types/analytics.ts +497 -0
- package/src/types/brands.ts +228 -0
- package/src/types/competitors.ts +196 -0
- package/src/types/identity.ts +55 -0
- package/src/types/index.ts +163 -0
- package/src/types/models.ts +40 -0
- package/src/types/opportunities.ts +84 -0
- package/src/types/organizations.ts +157 -0
- package/src/types/prompts.ts +231 -0
- package/src/types/reports.ts +266 -0
- package/src/types/runs.ts +131 -0
- package/src/types/shared/brand-id-path.ts +2 -0
- package/src/types/shared/conflict-error.ts +7 -0
- package/src/types/shared/error.ts +29 -0
- package/src/types/shared/limit.ts +8 -0
- package/src/types/shared/mention-entry.ts +21 -0
- package/src/types/shared/model-filter.ts +15 -0
- package/src/types/shared/not-found-error.ts +4 -0
- package/src/types/shared/page.ts +7 -0
- package/src/types/shared/pagination.ts +7 -0
- package/src/types/shared/validation-error.ts +4 -0
- package/src/types/snapshots.ts +88 -0
- package/src/types/tags.ts +47 -0
- package/src/types/tools.ts +78 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { awaitedResult } from './call-promise';
|
|
2
|
+
import type { Client } from './client';
|
|
3
|
+
import { APIError, ElmoError } from './errors';
|
|
4
|
+
import { LazyPromise, unattended } from './lazy-promise';
|
|
5
|
+
import type {
|
|
6
|
+
CallableDescriptor,
|
|
7
|
+
ErrorMode,
|
|
8
|
+
PageEnvelope,
|
|
9
|
+
PageResult,
|
|
10
|
+
PaginationDescriptor,
|
|
11
|
+
PaginationStyle,
|
|
12
|
+
ResolvedOptions,
|
|
13
|
+
Result,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One page of a paginated call. Iterating a page walks from it to the end of
|
|
18
|
+
* the collection.
|
|
19
|
+
*/
|
|
20
|
+
export interface Page<TItem = unknown, TBody = unknown> extends AsyncIterable<TItem> {
|
|
21
|
+
/** The whole reply body, counts and all. */
|
|
22
|
+
readonly body: TBody;
|
|
23
|
+
/** The items on this page. */
|
|
24
|
+
readonly data: ReadonlyArray<TItem>;
|
|
25
|
+
/** The next page. Throws if `hasNextPage()` is `false`. */
|
|
26
|
+
getNextPage(): Promise<Page<TItem, TBody>>;
|
|
27
|
+
/** Whether a page follows this one. It sends no request. */
|
|
28
|
+
hasNextPage(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Where the page after this one begins, or `undefined` when none follows.
|
|
31
|
+
* Pass it back as `pageParam` to resume from here.
|
|
32
|
+
*/
|
|
33
|
+
readonly nextPageParam: unknown;
|
|
34
|
+
/** The HTTP status of the reply that carried this page. */
|
|
35
|
+
readonly status: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Walk {
|
|
39
|
+
readonly cursor: unknown;
|
|
40
|
+
readonly descriptor: CallableDescriptor;
|
|
41
|
+
readonly options: ResolvedOptions;
|
|
42
|
+
readonly position: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readPath(value: unknown, path: string | undefined): unknown {
|
|
46
|
+
if (!path) return value;
|
|
47
|
+
let current = value;
|
|
48
|
+
for (const key of path.split('.')) {
|
|
49
|
+
if (current === null || typeof current !== 'object') return undefined;
|
|
50
|
+
current = (current as Record<string, unknown>)[key];
|
|
51
|
+
}
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readItems(data: unknown, pagination: PaginationDescriptor): ReadonlyArray<unknown> {
|
|
56
|
+
const items = readPath(data, pagination.items);
|
|
57
|
+
return Array.isArray(items) ? items : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function withPosition(
|
|
61
|
+
options: ResolvedOptions,
|
|
62
|
+
pagination: PaginationDescriptor,
|
|
63
|
+
value: unknown,
|
|
64
|
+
): ResolvedOptions {
|
|
65
|
+
const name = pagination.param;
|
|
66
|
+
if (!name) return options;
|
|
67
|
+
if ((pagination.in ?? 'query') === 'body') {
|
|
68
|
+
return { ...options, body: { ...(options.body as object | undefined), [name]: value } };
|
|
69
|
+
}
|
|
70
|
+
return { ...options, query: { ...options.query, [name]: value } };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resumeAt(walk: Walk, pagination: PaginationDescriptor, param: unknown): Walk {
|
|
74
|
+
if (pagination.style === 'link') {
|
|
75
|
+
const options = { ...walk.options };
|
|
76
|
+
delete options.query;
|
|
77
|
+
return { ...walk, descriptor: { ...walk.descriptor, address: param as string }, options };
|
|
78
|
+
}
|
|
79
|
+
const options = withPosition(walk.options, pagination, param);
|
|
80
|
+
if (pagination.style === 'cursor') return { ...walk, cursor: param, options };
|
|
81
|
+
return { ...walk, options, position: param as number };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readCount(data: unknown, path: string | undefined): number | undefined {
|
|
85
|
+
if (!path) return undefined;
|
|
86
|
+
const value = readPath(data, path);
|
|
87
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function startOf(pagination: PaginationDescriptor): number {
|
|
92
|
+
return pagination.start ?? (pagination.style === 'page' ? 1 : 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stepCounted(
|
|
96
|
+
walk: Walk,
|
|
97
|
+
pagination: PaginationDescriptor,
|
|
98
|
+
result: Result,
|
|
99
|
+
count: number,
|
|
100
|
+
limit: number | undefined,
|
|
101
|
+
): unknown {
|
|
102
|
+
if (!count) return undefined;
|
|
103
|
+
|
|
104
|
+
const size = readCount(result.data, pagination.size) ?? limit;
|
|
105
|
+
const position = walk.position + (pagination.style === 'page' ? 1 : size || count);
|
|
106
|
+
const start = startOf(pagination);
|
|
107
|
+
|
|
108
|
+
const pages = pagination.style === 'page' ? readCount(result.data, pagination.pages) : undefined;
|
|
109
|
+
const total = readCount(result.data, pagination.total);
|
|
110
|
+
if (pages === undefined && total === undefined && size !== undefined && count < size) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
if (pages !== undefined && position - start >= pages) return undefined;
|
|
114
|
+
if (total !== undefined) {
|
|
115
|
+
const read = pagination.style === 'page' ? (position - start) * (size ?? count) : position;
|
|
116
|
+
if (read >= total) return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return position;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type Stepper = (
|
|
123
|
+
walk: Walk,
|
|
124
|
+
pagination: PaginationDescriptor,
|
|
125
|
+
result: Result,
|
|
126
|
+
count: number,
|
|
127
|
+
limit: number | undefined,
|
|
128
|
+
) => unknown;
|
|
129
|
+
|
|
130
|
+
const STEPPERS: Partial<Readonly<Record<PaginationStyle, Stepper>>> = { page: stepCounted };
|
|
131
|
+
|
|
132
|
+
const SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
|
|
133
|
+
|
|
134
|
+
function pageURL(address: unknown): URL | undefined {
|
|
135
|
+
if (typeof address !== 'string') return undefined;
|
|
136
|
+
try {
|
|
137
|
+
const url = new URL(address, (globalThis as { location?: { href?: string } }).location?.href);
|
|
138
|
+
return url.origin === 'null' ? undefined : url;
|
|
139
|
+
} catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function followLink(client: Client, walk: Walk, link: string): string {
|
|
145
|
+
const own = pageURL(client.resolveAddress(walk.descriptor, walk.options));
|
|
146
|
+
if (!own) {
|
|
147
|
+
if (!SCHEME_RE.test(link) && !link.startsWith('//')) return link;
|
|
148
|
+
throw new ElmoError(
|
|
149
|
+
'Pagination stopped: the next page names a host, and no base URL says which host this client sends its credentials to.',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const target = new URL(link, own);
|
|
153
|
+
if (target.origin !== own.origin) {
|
|
154
|
+
throw new ElmoError(
|
|
155
|
+
`Pagination stopped: the next page is on ${target.origin}, and this client sends its credentials only to ${own.origin}.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return SCHEME_RE.test(link) ? link : target.href;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function saysMore(data: unknown, pagination: PaginationDescriptor): boolean {
|
|
162
|
+
return !pagination.more || readPath(data, pagination.more) !== false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function stepWalk(
|
|
166
|
+
client: Client,
|
|
167
|
+
walk: Walk,
|
|
168
|
+
pagination: PaginationDescriptor,
|
|
169
|
+
result: Result,
|
|
170
|
+
count: number,
|
|
171
|
+
limit: number | undefined,
|
|
172
|
+
): unknown {
|
|
173
|
+
const step = STEPPERS[pagination.style];
|
|
174
|
+
if (!step) {
|
|
175
|
+
throw new ElmoError(
|
|
176
|
+
`Cannot walk "${walk.descriptor.address}": this client does not page by "${pagination.style}".`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!saysMore(result.data, pagination)) return undefined;
|
|
180
|
+
const next = step(walk, pagination, result, count, limit);
|
|
181
|
+
return pagination.style === 'link' && next !== undefined
|
|
182
|
+
? followLink(client, walk, next as string)
|
|
183
|
+
: next;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function* walkFrom<TItem>(page: Page<TItem>, maxPages: number): AsyncGenerator<TItem> {
|
|
187
|
+
let current = page;
|
|
188
|
+
for (let index = 1; ; index++) {
|
|
189
|
+
for (const item of current.data) yield item;
|
|
190
|
+
if (index >= maxPages || !current.hasNextPage()) return;
|
|
191
|
+
current = await current.getNextPage();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function loadPage<TItem, TBody>(
|
|
196
|
+
client: Client,
|
|
197
|
+
walk: Walk,
|
|
198
|
+
pagination: PaginationDescriptor,
|
|
199
|
+
limit: number | undefined,
|
|
200
|
+
maxPages: number,
|
|
201
|
+
): Promise<Page<TItem, TBody>> {
|
|
202
|
+
const result = (await client.call(walk.descriptor, walk.options)) as Result;
|
|
203
|
+
if (result.error !== undefined) throw result.error;
|
|
204
|
+
|
|
205
|
+
const data = readItems(result.data, pagination) as ReadonlyArray<TItem>;
|
|
206
|
+
const param = stepWalk(client, walk, pagination, result, data.length, limit);
|
|
207
|
+
|
|
208
|
+
const page: Page<TItem, TBody> = {
|
|
209
|
+
[Symbol.asyncIterator]: () => walkFrom(page, maxPages),
|
|
210
|
+
body: result.data as TBody,
|
|
211
|
+
data,
|
|
212
|
+
getNextPage: () => {
|
|
213
|
+
if (param === undefined) {
|
|
214
|
+
return Promise.reject(
|
|
215
|
+
new ElmoError(
|
|
216
|
+
`"${walk.descriptor.address}" has no page after this one. Check \`hasNextPage()\` first.`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return loadPage<TItem, TBody>(
|
|
221
|
+
client,
|
|
222
|
+
resumeAt(walk, pagination, param),
|
|
223
|
+
pagination,
|
|
224
|
+
limit,
|
|
225
|
+
maxPages,
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
hasNextPage: () => param !== undefined,
|
|
229
|
+
nextPageParam: param,
|
|
230
|
+
status: result.status as number,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return page;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const DEFAULT_MAX_PAGES = 1000;
|
|
237
|
+
|
|
238
|
+
function maxPagesFor(client: Client, options: ResolvedOptions): number {
|
|
239
|
+
return (options['maxPages'] ?? client.setup.defaults['maxPages'] ?? DEFAULT_MAX_PAGES) as number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function openedAt(options: ResolvedOptions, pagination: PaginationDescriptor): unknown {
|
|
243
|
+
if (!pagination.param) return undefined;
|
|
244
|
+
const layer = options[pagination.in ?? 'query'] as Record<string, unknown> | undefined;
|
|
245
|
+
return layer?.[pagination.param];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function paginationOf(callable: CallableDescriptor): PaginationDescriptor {
|
|
249
|
+
if (callable.pagination) return callable.pagination;
|
|
250
|
+
throw new ElmoError(`"${callable.address}" is not a paged call, so it has no pages to walk.`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function openWalk(callable: CallableDescriptor, options: ResolvedOptions): Walk {
|
|
254
|
+
const pagination = paginationOf(callable);
|
|
255
|
+
const opened = openedAt(options, pagination);
|
|
256
|
+
return {
|
|
257
|
+
cursor: pagination.style === 'cursor' ? opened : undefined,
|
|
258
|
+
descriptor: callable,
|
|
259
|
+
options: { ...options, raw: true },
|
|
260
|
+
position: typeof opened === 'number' && Number.isFinite(opened) ? opened : startOf(pagination),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function pageStream<TItem, TBody>(
|
|
265
|
+
load: () => Promise<Page<TItem, TBody>>,
|
|
266
|
+
maxPages: number,
|
|
267
|
+
): AsyncIterable<Page<TItem, TBody>> {
|
|
268
|
+
return {
|
|
269
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<Page<TItem, TBody>> {
|
|
270
|
+
let current = await load();
|
|
271
|
+
for (let index = 1; ; index++) {
|
|
272
|
+
yield current;
|
|
273
|
+
if (index >= maxPages || !current.hasNextPage()) return;
|
|
274
|
+
current = await current.getNextPage();
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requestedLimit(
|
|
281
|
+
options: ResolvedOptions,
|
|
282
|
+
pagination: PaginationDescriptor,
|
|
283
|
+
): number | undefined {
|
|
284
|
+
if (!pagination.limitParam) return undefined;
|
|
285
|
+
const layer = options[pagination.in ?? 'query'] as Record<string, unknown> | undefined;
|
|
286
|
+
const limit = Number(layer?.[pagination.limitParam]);
|
|
287
|
+
return Number.isFinite(limit) && limit > 0 ? limit : undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* A paginated call. Awaiting it gives the first page, or its result envelope
|
|
292
|
+
* under the `'return'` error mode. Iterating it gives every item on every page,
|
|
293
|
+
* and throws on a failed page under either mode.
|
|
294
|
+
*/
|
|
295
|
+
export class PagePromise<
|
|
296
|
+
TItem = unknown,
|
|
297
|
+
TBody = unknown,
|
|
298
|
+
TErrors = unknown,
|
|
299
|
+
TAwaited = Page<TItem, TBody>,
|
|
300
|
+
TMode extends ErrorMode = 'throw',
|
|
301
|
+
>
|
|
302
|
+
extends LazyPromise<PageResult<TAwaited, TErrors, TMode>>
|
|
303
|
+
implements AsyncIterable<TItem>
|
|
304
|
+
{
|
|
305
|
+
private envelope: Promise<PageEnvelope<TAwaited, TErrors>> | undefined;
|
|
306
|
+
|
|
307
|
+
private readonly callable: CallableDescriptor;
|
|
308
|
+
private readonly client: Client;
|
|
309
|
+
private readonly limit: number | undefined;
|
|
310
|
+
private readonly maxPages: number;
|
|
311
|
+
private readonly options: ResolvedOptions;
|
|
312
|
+
private readonly pagination: PaginationDescriptor;
|
|
313
|
+
private readonly project: (page: Page<TItem, TBody>) => TAwaited;
|
|
314
|
+
|
|
315
|
+
private readonly first: Promise<Page<TItem, TBody>>;
|
|
316
|
+
|
|
317
|
+
constructor(
|
|
318
|
+
client: Client,
|
|
319
|
+
callable: CallableDescriptor,
|
|
320
|
+
options: ResolvedOptions,
|
|
321
|
+
project: (page: Page<TItem, TBody>) => TAwaited,
|
|
322
|
+
) {
|
|
323
|
+
super();
|
|
324
|
+
this.callable = callable;
|
|
325
|
+
this.client = client;
|
|
326
|
+
this.options = options;
|
|
327
|
+
this.pagination = paginationOf(callable);
|
|
328
|
+
this.limit = requestedLimit(options, this.pagination);
|
|
329
|
+
this.maxPages = maxPagesFor(client, options);
|
|
330
|
+
this.project = project;
|
|
331
|
+
const opened = openWalk(this.callable, this.options);
|
|
332
|
+
const param = options['pageParam'];
|
|
333
|
+
this.first = unattended(
|
|
334
|
+
loadPage<TItem, TBody>(
|
|
335
|
+
this.client,
|
|
336
|
+
param === undefined ? opened : resumeAt(opened, this.pagination, param),
|
|
337
|
+
this.pagination,
|
|
338
|
+
this.limit,
|
|
339
|
+
this.maxPages,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Every item, page after page, up to `maxPages`. */
|
|
345
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<TItem> {
|
|
346
|
+
yield* walkFrom(await this.first, this.maxPages);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Every page, up to `maxPages`. */
|
|
350
|
+
iterPages(): AsyncIterable<Page<TItem, TBody>> {
|
|
351
|
+
return pageStream(() => this.first, this.maxPages);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The whole result envelope. A failure of the call is returned on it, not
|
|
356
|
+
* thrown. What one of your own functions throws, such as a hook, is thrown
|
|
357
|
+
* as it is.
|
|
358
|
+
*/
|
|
359
|
+
result(): Promise<PageEnvelope<TAwaited, TErrors>> {
|
|
360
|
+
return (this.envelope ??= this.read());
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The page alone, with a failure thrown rather than handed back. */
|
|
364
|
+
async unwrap(): Promise<TAwaited> {
|
|
365
|
+
const result = await this.result();
|
|
366
|
+
if (result.ok === false) throw result.error;
|
|
367
|
+
return result.data;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async read(): Promise<PageEnvelope<TAwaited, TErrors>> {
|
|
371
|
+
try {
|
|
372
|
+
const page = await this.first;
|
|
373
|
+
return { data: this.project(page), ok: true, status: page.status };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (!(error instanceof ElmoError)) throw error;
|
|
376
|
+
const status = error instanceof APIError ? error.status : undefined;
|
|
377
|
+
return { error, ok: false, status } as PageEnvelope<TAwaited, TErrors>;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
protected override settle(): Promise<PageResult<TAwaited, TErrors, TMode>> {
|
|
382
|
+
return this.result().then((result) => awaitedResult(this.client, this.options, result));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function pages<
|
|
387
|
+
TItem = unknown,
|
|
388
|
+
TBody = unknown,
|
|
389
|
+
TErrors = unknown,
|
|
390
|
+
TMode extends ErrorMode = 'throw',
|
|
391
|
+
>(
|
|
392
|
+
client: Client,
|
|
393
|
+
callable: CallableDescriptor,
|
|
394
|
+
options: ResolvedOptions = {},
|
|
395
|
+
): PagePromise<TItem, TBody, TErrors, Page<TItem, TBody>, TMode> {
|
|
396
|
+
return new PagePromise<TItem, TBody, TErrors, Page<TItem, TBody>, TMode>(
|
|
397
|
+
client,
|
|
398
|
+
callable,
|
|
399
|
+
options,
|
|
400
|
+
(page) => page,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ResolvedOptions } from './types';
|
|
2
|
+
|
|
3
|
+
export type ExtraKeys = 'body' | 'query';
|
|
4
|
+
|
|
5
|
+
export type FlatLayer = Readonly<Record<string, string>> | ReadonlyArray<string>;
|
|
6
|
+
|
|
7
|
+
export interface FlatFields {
|
|
8
|
+
body?: FlatLayer;
|
|
9
|
+
path?: FlatLayer;
|
|
10
|
+
query?: FlatLayer;
|
|
11
|
+
whole?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LAYERS = ['path', 'query'] as const;
|
|
15
|
+
|
|
16
|
+
export function mergeParams(
|
|
17
|
+
inputs: Readonly<Record<string, unknown>> | undefined,
|
|
18
|
+
options: Readonly<Record<string, unknown>> | undefined,
|
|
19
|
+
): ResolvedOptions {
|
|
20
|
+
const merged: Record<string, unknown> = { ...inputs, ...options };
|
|
21
|
+
if (inputs && options) {
|
|
22
|
+
for (const layer of LAYERS) {
|
|
23
|
+
const declared = inputs[layer] as Record<string, unknown> | undefined;
|
|
24
|
+
const given = options[layer] as Record<string, unknown> | undefined;
|
|
25
|
+
if (!declared || !given) continue;
|
|
26
|
+
const both: Record<string, unknown> = { ...declared };
|
|
27
|
+
for (const [name, value] of Object.entries(given)) {
|
|
28
|
+
if (value !== undefined) both[name] = value;
|
|
29
|
+
}
|
|
30
|
+
merged[layer] = both;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return merged as ResolvedOptions;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Placement {
|
|
37
|
+
in: keyof FlatFields;
|
|
38
|
+
map: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const placed = new WeakMap<FlatFields, ReadonlyMap<string, Placement>>();
|
|
42
|
+
|
|
43
|
+
function placements(fields: FlatFields): ReadonlyMap<string, Placement> {
|
|
44
|
+
let known = placed.get(fields);
|
|
45
|
+
if (!known) {
|
|
46
|
+
const built = new Map<string, Placement>();
|
|
47
|
+
for (const layer of Object.keys(fields) as Array<keyof FlatFields>) {
|
|
48
|
+
const names = fields[layer];
|
|
49
|
+
if (typeof names === 'string') built.set(names, { in: layer, map: names });
|
|
50
|
+
else if (Array.isArray(names)) {
|
|
51
|
+
for (const key of names) built.set(key, { in: layer, map: key });
|
|
52
|
+
} else if (names) {
|
|
53
|
+
for (const [key, wire] of Object.entries(names)) built.set(key, { in: layer, map: wire });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
known = built;
|
|
57
|
+
placed.set(fields, built);
|
|
58
|
+
}
|
|
59
|
+
return known;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function placeParams(
|
|
63
|
+
options: Readonly<Record<string, unknown>> | undefined,
|
|
64
|
+
fields: FlatFields,
|
|
65
|
+
extras?: ExtraKeys,
|
|
66
|
+
): ResolvedOptions {
|
|
67
|
+
const named = placements(fields);
|
|
68
|
+
const grouped: Record<string, unknown> = {};
|
|
69
|
+
const given: Record<string, unknown> = {};
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of Object.entries(options ?? {})) {
|
|
72
|
+
const place = named.get(key);
|
|
73
|
+
|
|
74
|
+
if (!place) {
|
|
75
|
+
if (extras === undefined) given[key] = value;
|
|
76
|
+
else ((grouped[extras] ??= {}) as Record<string, unknown>)[key] = value;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (place.in === 'whole') {
|
|
81
|
+
grouped['body'] = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
((grouped[place.in] ??= {}) as Record<string, unknown>)[place.map] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return mergeParams(grouped, given);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function groupParams(
|
|
92
|
+
options: Readonly<Record<string, unknown>> | undefined,
|
|
93
|
+
layer?: ExtraKeys,
|
|
94
|
+
): ResolvedOptions {
|
|
95
|
+
if (layer === undefined) return { ...options } as ResolvedOptions;
|
|
96
|
+
if (!options || !Object.keys(options).length) return {};
|
|
97
|
+
return { [layer]: { ...options } } as ResolvedOptions;
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnyTransport, Binding, Interaction } from './types';
|
|
2
|
+
|
|
3
|
+
export type Protocol = 'channel' | 'rest' | 'websocket';
|
|
4
|
+
|
|
5
|
+
export interface ProtocolSetup {
|
|
6
|
+
binding: Binding;
|
|
7
|
+
transport: AnyTransport;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Protocols = Partial<Record<Protocol, ProtocolSetup>>;
|
|
11
|
+
|
|
12
|
+
export function onlyProtocol(
|
|
13
|
+
protocols: Protocols,
|
|
14
|
+
_interaction: Interaction | undefined,
|
|
15
|
+
): ProtocolSetup {
|
|
16
|
+
return Object.values(protocols)[0]!;
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function scalarText(value: unknown): string | undefined {
|
|
2
|
+
return value instanceof Date ? value.toISOString() : undefined;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function toText(value: unknown): string {
|
|
6
|
+
return typeof value === 'string' ? value : (scalarText(value) ?? String(value));
|
|
7
|
+
}
|