@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,11 @@
|
|
|
1
|
+
import { ElmoError } from './errors';
|
|
2
|
+
import type { ResolvedOptions } from './types';
|
|
3
|
+
|
|
4
|
+
export function refuseBrowser(options: ResolvedOptions): void {
|
|
5
|
+
if (options['dangerouslyAllowBrowser'] === true) return;
|
|
6
|
+
const scope = globalThis as { document?: unknown; window?: unknown };
|
|
7
|
+
if (scope.window === undefined || scope.document === undefined) return;
|
|
8
|
+
throw new ElmoError(
|
|
9
|
+
'This client was built in a browser, where every visitor to the page can read the credential it sends. Build it on a server instead. If the credential is meant to be public, or a proxy adds it, pass `dangerouslyAllowBrowser: true`.',
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Client } from './client';
|
|
2
|
+
import type { Exchange } from './dispatch';
|
|
3
|
+
import { ElmoError } from './errors';
|
|
4
|
+
import { LazyPromise, unattended } from './lazy-promise';
|
|
5
|
+
import type {
|
|
6
|
+
CallableDescriptor,
|
|
7
|
+
CallResult,
|
|
8
|
+
ErrorMode,
|
|
9
|
+
ResolvedOptions,
|
|
10
|
+
ResultEnvelope,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
export function awaitedResult<T>(
|
|
14
|
+
client: Client,
|
|
15
|
+
options: ResolvedOptions,
|
|
16
|
+
result: { data?: unknown; error?: unknown; ok?: boolean },
|
|
17
|
+
): T {
|
|
18
|
+
if ((options.errors ?? client.setup.defaults.errors) === 'return') return result as T;
|
|
19
|
+
if (result.ok === false) throw result.error;
|
|
20
|
+
return result.data as T;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A call already sent, whose reply is not yet read. */
|
|
24
|
+
export class CallPromise<
|
|
25
|
+
TResponses = unknown,
|
|
26
|
+
TErrors = unknown,
|
|
27
|
+
TMode extends ErrorMode = 'throw',
|
|
28
|
+
> extends LazyPromise<CallResult<TResponses, TErrors, TMode>> {
|
|
29
|
+
protected readonly client: Client;
|
|
30
|
+
protected readonly exchange: Promise<Exchange>;
|
|
31
|
+
protected readonly options: ResolvedOptions;
|
|
32
|
+
|
|
33
|
+
private envelope: Promise<ResultEnvelope<TResponses, TErrors>> | undefined;
|
|
34
|
+
|
|
35
|
+
constructor(client: Client, callable: CallableDescriptor, options: ResolvedOptions = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.client = client;
|
|
38
|
+
this.options = options;
|
|
39
|
+
this.exchange = unattended(client.exchange(callable, { ...options, envelope: true }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The whole result envelope. A failure of the call is returned on it, not
|
|
44
|
+
* thrown. What one of your own functions throws, such as a hook, is thrown
|
|
45
|
+
* as it is.
|
|
46
|
+
*/
|
|
47
|
+
result(): Promise<ResultEnvelope<TResponses, TErrors>> {
|
|
48
|
+
return (this.envelope ??= this.read());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The payload alone, with a failure thrown rather than handed back. */
|
|
52
|
+
async unwrap(): Promise<CallResult<TResponses, TErrors, 'throw'>> {
|
|
53
|
+
const result = await this.result();
|
|
54
|
+
if (result.ok === false) throw result.error;
|
|
55
|
+
return result.data as CallResult<TResponses, TErrors, 'throw'>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async read(): Promise<ResultEnvelope<TResponses, TErrors>> {
|
|
59
|
+
try {
|
|
60
|
+
return await this.decode();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!(error instanceof ElmoError)) throw error;
|
|
63
|
+
return { error, ok: false } as ResultEnvelope<TResponses, TErrors>;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected override settle(): Promise<CallResult<TResponses, TErrors, TMode>> {
|
|
68
|
+
return this.result().then((result) => awaitedResult(this.client, this.options, result));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected async decode(): Promise<ResultEnvelope<TResponses, TErrors>> {
|
|
72
|
+
return (await this.exchange).read() as Promise<ResultEnvelope<TResponses, TErrors>>;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What a call resolves with where its `Response` is asked for as well. */
|
|
77
|
+
export interface WithResponse<TData> {
|
|
78
|
+
/** What the call resolves with on its own. */
|
|
79
|
+
data: TData;
|
|
80
|
+
/** The request id to quote when reporting a problem with the call. */
|
|
81
|
+
requestId: string | undefined;
|
|
82
|
+
/** The `Response` that carried `data`. Its body is already read. */
|
|
83
|
+
response: Response;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A call already sent, whose `Response` can be read as well as its payload. */
|
|
87
|
+
export class RestCallPromise<
|
|
88
|
+
TResponses = unknown,
|
|
89
|
+
TErrors = unknown,
|
|
90
|
+
TMode extends ErrorMode = 'throw',
|
|
91
|
+
> extends CallPromise<TResponses, TErrors, TMode> {
|
|
92
|
+
private claimed = false;
|
|
93
|
+
|
|
94
|
+
private unread: Promise<Response> | undefined;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The `Response` itself, with its body unread and yours to read once. An
|
|
98
|
+
* error status does not throw here. A call that got no response does.
|
|
99
|
+
*/
|
|
100
|
+
asResponse(): Promise<Response> {
|
|
101
|
+
return (this.unread ??= this.claim());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The decoded body, the `Response` that carried it, and the request id. */
|
|
105
|
+
async withResponse(): Promise<WithResponse<CallResult<TResponses, TErrors, TMode>>> {
|
|
106
|
+
const data = await (this as CallPromise<TResponses, TErrors, TMode>);
|
|
107
|
+
const exchange = await this.exchange;
|
|
108
|
+
return { data, requestId: exchange.requestId(), response: exchange.response as Response };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected override async decode(): Promise<ResultEnvelope<TResponses, TErrors>> {
|
|
112
|
+
const exchange = await this.exchange;
|
|
113
|
+
const { response } = exchange;
|
|
114
|
+
if (!this.claimed || !response) return super.decode();
|
|
115
|
+
if (!response.bodyUsed) {
|
|
116
|
+
return exchange.read(response.clone()) as Promise<ResultEnvelope<TResponses, TErrors>>;
|
|
117
|
+
}
|
|
118
|
+
throw new ElmoError(
|
|
119
|
+
'The body of this call was already read through asResponse(). Use withResponse() instead, which gives the body and the reply from one call.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async claim(): Promise<Response> {
|
|
124
|
+
const { response } = await this.exchange;
|
|
125
|
+
if (!response) {
|
|
126
|
+
throw new ElmoError(
|
|
127
|
+
'This call was answered without a `Response`, so there is none to hand back.',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!response.bodyUsed) this.claimed = true;
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function callPromise<
|
|
136
|
+
TResponses = unknown,
|
|
137
|
+
TErrors = unknown,
|
|
138
|
+
TMode extends ErrorMode = 'throw',
|
|
139
|
+
>(
|
|
140
|
+
client: Client,
|
|
141
|
+
callable: CallableDescriptor,
|
|
142
|
+
options: ResolvedOptions = {},
|
|
143
|
+
): RestCallPromise<TResponses, TErrors, TMode> {
|
|
144
|
+
return new RestCallPromise<TResponses, TErrors, TMode>(client, callable, options);
|
|
145
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CallableDescriptor, Interaction } from './types';
|
|
2
|
+
|
|
3
|
+
export type CallableInput = Omit<CallableDescriptor, 'interaction'> & {
|
|
4
|
+
readonly interaction?: Interaction;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function callable<T extends CallableInput>(
|
|
8
|
+
descriptor: T,
|
|
9
|
+
): T & { readonly interaction: Interaction } {
|
|
10
|
+
return { interaction: 'unary', ...descriptor };
|
|
11
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mergeConfigs, mergeDefaults } from './config';
|
|
2
|
+
import { applyAliases, applyCredentials } from './credentials';
|
|
3
|
+
import type { DispatchSetup, Exchange } from './dispatch';
|
|
4
|
+
import { dispatch, exchange, prepare } from './dispatch';
|
|
5
|
+
import { envDefaults } from './env';
|
|
6
|
+
import { ElmoError } from './errors';
|
|
7
|
+
import { onlyProtocol } from './protocol';
|
|
8
|
+
import type {
|
|
9
|
+
CallableDescriptor,
|
|
10
|
+
CallResult,
|
|
11
|
+
ErrorMode,
|
|
12
|
+
PreparedRequest,
|
|
13
|
+
ResolvedOptions,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
/** What every call is sent through. It holds the options all calls share. */
|
|
17
|
+
export interface Client {
|
|
18
|
+
/** @internal */
|
|
19
|
+
call<TData = unknown, TError = unknown, TMode extends ErrorMode = 'throw'>(
|
|
20
|
+
callable: CallableDescriptor,
|
|
21
|
+
options?: ResolvedOptions,
|
|
22
|
+
): Promise<CallResult<TData, TError, TMode>>;
|
|
23
|
+
/** @internal */
|
|
24
|
+
exchange(callable: CallableDescriptor, options?: ResolvedOptions): Promise<Exchange>;
|
|
25
|
+
/** @internal */
|
|
26
|
+
prepare<TAddress = unknown>(
|
|
27
|
+
callable: CallableDescriptor,
|
|
28
|
+
options?: ResolvedOptions,
|
|
29
|
+
): Promise<PreparedRequest<TAddress>>;
|
|
30
|
+
/** @internal */
|
|
31
|
+
resolveAddress<TAddress = unknown>(
|
|
32
|
+
callable: CallableDescriptor,
|
|
33
|
+
options?: ResolvedOptions,
|
|
34
|
+
): TAddress;
|
|
35
|
+
/** @internal */
|
|
36
|
+
readonly setup: DispatchSetup;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clientOf(setup: DispatchSetup): Client {
|
|
40
|
+
const client = {
|
|
41
|
+
call<TData = unknown, TError = unknown, TMode extends ErrorMode = 'throw'>(
|
|
42
|
+
callable: CallableDescriptor,
|
|
43
|
+
options?: ResolvedOptions,
|
|
44
|
+
): Promise<CallResult<TData, TError, TMode>> {
|
|
45
|
+
return dispatch(setup, callable, options) as Promise<CallResult<TData, TError, TMode>>;
|
|
46
|
+
},
|
|
47
|
+
exchange(callable: CallableDescriptor, options?: ResolvedOptions): Promise<Exchange> {
|
|
48
|
+
return exchange(setup, callable, options);
|
|
49
|
+
},
|
|
50
|
+
prepare<TAddress = unknown>(callable: CallableDescriptor, options: ResolvedOptions = {}) {
|
|
51
|
+
return prepare(setup, callable, options) as Promise<PreparedRequest<TAddress>>;
|
|
52
|
+
},
|
|
53
|
+
resolveAddress<TAddress = unknown>(
|
|
54
|
+
callable: CallableDescriptor,
|
|
55
|
+
options: ResolvedOptions = {},
|
|
56
|
+
) {
|
|
57
|
+
const { binding } = onlyProtocol(setup.protocols, callable.interaction);
|
|
58
|
+
return binding.resolveAddress(callable, mergeConfigs(setup.defaults, options)) as TAddress;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
return Object.defineProperty(client, 'setup', { value: setup }) as Client;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createClient(setup: DispatchSetup): Client {
|
|
65
|
+
return clientOf({
|
|
66
|
+
...setup,
|
|
67
|
+
defaults: envDefaults(setup.defaults, setup.env, setup.credentials),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function clientFor(
|
|
72
|
+
args: (Partial<ResolvedOptions> & { client?: Client; key?: string }) | undefined,
|
|
73
|
+
fallback?: Client,
|
|
74
|
+
): Client {
|
|
75
|
+
const { client, key: _key, ...args_ } = args ?? {};
|
|
76
|
+
const base = client ?? fallback;
|
|
77
|
+
if (!base) throw new ElmoError('This SDK has no client of its own. Pass one as `client`.');
|
|
78
|
+
const config = applyCredentials(applyAliases(args_, base.setup.aliases), base.setup.credentials);
|
|
79
|
+
if (client && !Object.keys(config).length) return client;
|
|
80
|
+
const defaults = mergeDefaults(base.setup.defaults, config);
|
|
81
|
+
base.setup.guard?.(defaults);
|
|
82
|
+
return clientOf({ ...base.setup, defaults });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function withClient<T extends object>(source: T, client: Client, key = 'client'): T {
|
|
86
|
+
const derived = Object.create(Object.getPrototypeOf(source)) as Record<string, Client>;
|
|
87
|
+
derived[key] = client;
|
|
88
|
+
return derived as T;
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mergeMetadata } from './metadata';
|
|
2
|
+
import type { ResolvedOptions } from './types';
|
|
3
|
+
|
|
4
|
+
type Layers = ReadonlyArray<Partial<ResolvedOptions> | undefined>;
|
|
5
|
+
|
|
6
|
+
export function foldRetry(sources: Layers, merged: ResolvedOptions): ResolvedOptions {
|
|
7
|
+
if (!sources.some((source) => source?.['retry'] !== undefined)) return merged;
|
|
8
|
+
let retry: unknown;
|
|
9
|
+
for (const source of sources) {
|
|
10
|
+
const own = source?.['retry'];
|
|
11
|
+
const count = source?.['maxRetries'];
|
|
12
|
+
const counted = typeof count === 'number' && Number.isFinite(count);
|
|
13
|
+
if (own === false) {
|
|
14
|
+
retry = false;
|
|
15
|
+
} else if (own !== undefined || counted) {
|
|
16
|
+
const rules: Record<string, unknown> = {
|
|
17
|
+
...(typeof retry === 'object' && retry),
|
|
18
|
+
...(typeof own === 'object' && own),
|
|
19
|
+
};
|
|
20
|
+
if (counted && (typeof own !== 'object' || !own || !('maxRetries' in own))) {
|
|
21
|
+
rules['maxRetries'] = count;
|
|
22
|
+
}
|
|
23
|
+
retry = rules;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
merged['retry'] = retry;
|
|
27
|
+
delete merged['maxRetries'];
|
|
28
|
+
return merged;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MERGED_LAYERS = ['path', 'query'] as const;
|
|
32
|
+
|
|
33
|
+
export function mergeLayers(sources: Layers, merged: ResolvedOptions): ResolvedOptions {
|
|
34
|
+
for (const key of MERGED_LAYERS) {
|
|
35
|
+
const layers = sources.filter((source) => source?.[key]);
|
|
36
|
+
if (!layers.length) continue;
|
|
37
|
+
const layer: Record<string, unknown> = {};
|
|
38
|
+
for (const source of layers) {
|
|
39
|
+
for (const [name, value] of Object.entries(source![key] as Record<string, unknown>)) {
|
|
40
|
+
if (value !== undefined) layer[name] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
merged[key] = layer;
|
|
44
|
+
}
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function merge(
|
|
49
|
+
sources: ReadonlyArray<Partial<ResolvedOptions> | undefined>,
|
|
50
|
+
keepAccessors: boolean,
|
|
51
|
+
): ResolvedOptions {
|
|
52
|
+
const merged: ResolvedOptions = {};
|
|
53
|
+
for (const source of sources) {
|
|
54
|
+
if (!source) continue;
|
|
55
|
+
if (keepAccessors) {
|
|
56
|
+
for (const [key, held] of Object.entries(Object.getOwnPropertyDescriptors(source))) {
|
|
57
|
+
if (held.get || held.value !== undefined) Object.defineProperty(merged, key, held);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
for (const key of Object.keys(source)) {
|
|
61
|
+
const value = source[key];
|
|
62
|
+
if (value !== undefined) merged[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
merged.headers = mergeMetadata(...sources.map((source) => source?.headers));
|
|
67
|
+
return foldRetry(sources, mergeLayers(sources, merged));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mergeConfigs(...sources: ReadonlyArray<Partial<ResolvedOptions> | undefined>) {
|
|
71
|
+
return merge(sources, false);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createConfig(overrides: Partial<ResolvedOptions> = {}): ResolvedOptions {
|
|
75
|
+
return mergeConfigs({ errors: 'throw', headers: {} }, overrides);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function baseURLOf(options: ResolvedOptions, fallback?: string): string | undefined {
|
|
79
|
+
return options.baseURL ?? fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function mergeDefaults(...sources: ReadonlyArray<Partial<ResolvedOptions> | undefined>) {
|
|
83
|
+
return merge(sources, true);
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CredentialSpec, ResolvedOptions } from './types';
|
|
2
|
+
|
|
3
|
+
const LAYER_ALIASES: Readonly<Record<string, string>> = {
|
|
4
|
+
defaultHeaders: 'headers',
|
|
5
|
+
defaultPath: 'path',
|
|
6
|
+
defaultQuery: 'query',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function applyAliases(
|
|
10
|
+
config: Partial<ResolvedOptions>,
|
|
11
|
+
aliases: Readonly<Record<string, string>> | undefined,
|
|
12
|
+
): Partial<ResolvedOptions> {
|
|
13
|
+
let applied: Record<string, unknown> | undefined;
|
|
14
|
+
for (const [alias, canonical] of Object.entries(
|
|
15
|
+
aliases ? { ...LAYER_ALIASES, ...aliases } : LAYER_ALIASES,
|
|
16
|
+
)) {
|
|
17
|
+
if (!(alias in config)) continue;
|
|
18
|
+
applied ??= { ...config };
|
|
19
|
+
if (applied[canonical] === undefined) applied[canonical] = applied[alias];
|
|
20
|
+
delete applied[alias];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (applied as Partial<ResolvedOptions> | undefined) ?? config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applyCredentials(
|
|
27
|
+
config: Partial<ResolvedOptions>,
|
|
28
|
+
credentials: Readonly<Record<string, CredentialSpec>> | undefined,
|
|
29
|
+
): Partial<ResolvedOptions> {
|
|
30
|
+
if (!credentials) return config;
|
|
31
|
+
|
|
32
|
+
let applied: Record<string, unknown> | undefined;
|
|
33
|
+
for (const [name, spec] of Object.entries(credentials)) {
|
|
34
|
+
if (spec.option === false || config[name] !== undefined || !(name in config)) continue;
|
|
35
|
+
applied ??= { ...config };
|
|
36
|
+
delete applied[name];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (applied as Partial<ResolvedOptions> | undefined) ?? config;
|
|
40
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { bufferReply } from './body';
|
|
2
|
+
import { mergeConfigs } from './config';
|
|
3
|
+
import { applyAliases, applyCredentials } from './credentials';
|
|
4
|
+
import {
|
|
5
|
+
ElmoError,
|
|
6
|
+
sniffRequestId,
|
|
7
|
+
unansweredError,
|
|
8
|
+
unsendableHeader,
|
|
9
|
+
UnsupportedInteractionError,
|
|
10
|
+
} from './errors';
|
|
11
|
+
import { FeatureRunner } from './features';
|
|
12
|
+
import { mergeMetadata, unsendableName } from './metadata';
|
|
13
|
+
import type { Protocols } from './protocol';
|
|
14
|
+
import { onlyProtocol } from './protocol';
|
|
15
|
+
import type {
|
|
16
|
+
AnyTransport,
|
|
17
|
+
Binding,
|
|
18
|
+
CallableDescriptor,
|
|
19
|
+
Codec,
|
|
20
|
+
CredentialSpec,
|
|
21
|
+
Feature,
|
|
22
|
+
FeatureContext,
|
|
23
|
+
PreparedRequest,
|
|
24
|
+
ResolvedOptions,
|
|
25
|
+
Result,
|
|
26
|
+
Send,
|
|
27
|
+
} from './types';
|
|
28
|
+
import { encodeNamedBody } from '../codec/registry';
|
|
29
|
+
|
|
30
|
+
export interface DispatchSetup {
|
|
31
|
+
accept?: string;
|
|
32
|
+
aliases?: Readonly<Record<string, string>>;
|
|
33
|
+
codecs: ReadonlyArray<Codec>;
|
|
34
|
+
credentials?: Readonly<Record<string, CredentialSpec>>;
|
|
35
|
+
defaults: ResolvedOptions;
|
|
36
|
+
env?: Readonly<Record<string, string>>;
|
|
37
|
+
features?: ReadonlyArray<Feature>;
|
|
38
|
+
guard?: (options: ResolvedOptions) => void;
|
|
39
|
+
protocols: Protocols;
|
|
40
|
+
transports?: (name: string) => AnyTransport;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Exchange {
|
|
44
|
+
read(from?: Response): Promise<unknown>;
|
|
45
|
+
requestId(): string | undefined;
|
|
46
|
+
readonly response: Response | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Sent {
|
|
50
|
+
binding: Binding;
|
|
51
|
+
ctx: FeatureContext;
|
|
52
|
+
result: Result;
|
|
53
|
+
runner: FeatureRunner;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readReply(
|
|
57
|
+
setup: DispatchSetup,
|
|
58
|
+
sent: Sent,
|
|
59
|
+
request: PreparedRequest,
|
|
60
|
+
from?: Response,
|
|
61
|
+
): Promise<unknown> {
|
|
62
|
+
const { binding, ctx, runner } = sent;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const raw = from ?? sent.result.response;
|
|
66
|
+
let result =
|
|
67
|
+
request.options.unread || !raw
|
|
68
|
+
? sent.result
|
|
69
|
+
: await binding.readResult!(raw, request, setup.codecs);
|
|
70
|
+
|
|
71
|
+
result = await runner.result(result, request, ctx);
|
|
72
|
+
|
|
73
|
+
const failure = binding.readError?.(result, request) ?? result.error;
|
|
74
|
+
|
|
75
|
+
if (request.options.raw) {
|
|
76
|
+
return failure === undefined ? result : { ...result, error: failure };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (failure !== undefined) {
|
|
80
|
+
if (request.options.errors !== 'return' && !request.options.envelope) throw failure;
|
|
81
|
+
return {
|
|
82
|
+
data: undefined,
|
|
83
|
+
error: await runner.error(failure, request, ctx),
|
|
84
|
+
ok: false,
|
|
85
|
+
status: result.status,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return request.options.errors === 'return' || request.options.envelope
|
|
90
|
+
? { data: result.data, error: undefined, ok: true, status: result.status }
|
|
91
|
+
: result.data;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw await runner.error(error, request, ctx);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function rejectUnsendable(transport: AnyTransport, request: PreparedRequest): never {
|
|
98
|
+
throw new UnsupportedInteractionError(transport.name, request.interaction);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shellOf(request: PreparedRequest): PreparedRequest {
|
|
102
|
+
if (request.options.unread) return request;
|
|
103
|
+
return { ...request, options: { ...request.options, unread: true } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function transportFor(setup: DispatchSetup, named: unknown, fallback: AnyTransport): AnyTransport {
|
|
107
|
+
if (named === undefined) return fallback;
|
|
108
|
+
if (typeof named !== 'string') return named as AnyTransport;
|
|
109
|
+
const found = setup.transports?.(named);
|
|
110
|
+
if (!found) throw new ElmoError(`This client carries no transport named "${named}".`);
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function sendRequest(setup: DispatchSetup, request: PreparedRequest): Promise<Sent> {
|
|
115
|
+
const protocol = onlyProtocol(setup.protocols, request.interaction);
|
|
116
|
+
const { binding } = protocol;
|
|
117
|
+
const transport = transportFor(setup, request.options['transport'], protocol.transport);
|
|
118
|
+
const runner = new FeatureRunner(setup.features);
|
|
119
|
+
const ctx = { binding, transport };
|
|
120
|
+
|
|
121
|
+
const { unary } = transport;
|
|
122
|
+
if (request.interaction !== 'unary' || !unary) rejectUnsendable(transport, request);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
if (!binding.readResult) {
|
|
126
|
+
throw new ElmoError(
|
|
127
|
+
`The "${binding.name}" binding reads no reply, so it cannot answer this call.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const send: Send = async (attempt) => {
|
|
132
|
+
await runner.request(attempt, ctx);
|
|
133
|
+
const unsendable = unsendableName(attempt.meta);
|
|
134
|
+
if (unsendable !== undefined) throw unsendableHeader(unsendable);
|
|
135
|
+
attempt.log?.sending();
|
|
136
|
+
let raw: Response;
|
|
137
|
+
try {
|
|
138
|
+
raw = await unary.call(transport, attempt);
|
|
139
|
+
if (!request.options.unread) raw = await bufferReply(raw, attempt.signal);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw unansweredError(error);
|
|
142
|
+
}
|
|
143
|
+
return binding.readResult!(raw, shellOf(attempt), setup.codecs);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return { binding, ctx, result: await runner.send(send, ctx)(request), runner };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw await runner.error(error, request, ctx);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function execute(setup: DispatchSetup, request: PreparedRequest): Promise<unknown> {
|
|
153
|
+
return readReply(setup, await sendRequest(setup, request), request);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildRequestWithoutCookies(
|
|
157
|
+
setup: DispatchSetup,
|
|
158
|
+
callable: CallableDescriptor,
|
|
159
|
+
resolved: ResolvedOptions,
|
|
160
|
+
): PreparedRequest {
|
|
161
|
+
const interaction = callable.interaction ?? 'unary';
|
|
162
|
+
const { binding } = onlyProtocol(setup.protocols, interaction);
|
|
163
|
+
const encoded = encodeNamedBody(setup.codecs, callable.mediaType, resolved);
|
|
164
|
+
const meta = mergeMetadata(resolved.headers);
|
|
165
|
+
if (encoded?.contentType && meta['content-type'] === undefined) {
|
|
166
|
+
meta['content-type'] = encoded.contentType;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const accept = callable.accept ?? setup.accept ?? binding.accept;
|
|
170
|
+
if (accept && meta['accept'] === undefined) meta['accept'] = accept;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
address: binding.resolveAddress(callable, resolved),
|
|
174
|
+
body: encoded?.payload,
|
|
175
|
+
callable,
|
|
176
|
+
interaction,
|
|
177
|
+
meta,
|
|
178
|
+
options: resolved,
|
|
179
|
+
signal: resolved.signal,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function prepare<TAddress = unknown>(
|
|
184
|
+
setup: DispatchSetup,
|
|
185
|
+
callable: CallableDescriptor,
|
|
186
|
+
options: ResolvedOptions = {},
|
|
187
|
+
): Promise<PreparedRequest<TAddress>> {
|
|
188
|
+
const { binding, transport } = onlyProtocol(setup.protocols, callable.interaction);
|
|
189
|
+
const resolved = mergeConfigs(
|
|
190
|
+
setup.defaults,
|
|
191
|
+
applyCredentials(applyAliases(options, setup.aliases), setup.credentials),
|
|
192
|
+
);
|
|
193
|
+
const runner = new FeatureRunner(setup.features);
|
|
194
|
+
const ctx = { binding, transport };
|
|
195
|
+
|
|
196
|
+
await runner.options(resolved, callable, ctx);
|
|
197
|
+
|
|
198
|
+
const request = buildRequestWithoutCookies(setup, callable, resolved);
|
|
199
|
+
|
|
200
|
+
await runner.prepare(request, ctx);
|
|
201
|
+
return request as PreparedRequest<TAddress>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function dispatch(
|
|
205
|
+
setup: DispatchSetup,
|
|
206
|
+
callable: CallableDescriptor,
|
|
207
|
+
options: ResolvedOptions = {},
|
|
208
|
+
): Promise<unknown> {
|
|
209
|
+
return execute(setup, await prepare(setup, callable, options));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function exchange(
|
|
213
|
+
setup: DispatchSetup,
|
|
214
|
+
callable: CallableDescriptor,
|
|
215
|
+
options: ResolvedOptions = {},
|
|
216
|
+
): Promise<Exchange> {
|
|
217
|
+
const request = await prepare(setup, callable, options);
|
|
218
|
+
const sent = await sendRequest(setup, request);
|
|
219
|
+
let read: Promise<unknown> | undefined;
|
|
220
|
+
return {
|
|
221
|
+
read: (from) => (read ??= readReply(setup, sent, request, from)),
|
|
222
|
+
requestId: () => sent.result.response && sniffRequestId(sent.result.response),
|
|
223
|
+
response: sent.result.response,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { CredentialSpec, ResolvedOptions } from './types';
|
|
2
|
+
|
|
3
|
+
export function impliedEnvironment(..._tokens: ReadonlyArray<unknown>): undefined {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function readEnv(name: string): string | undefined {
|
|
8
|
+
const global = globalThis as {
|
|
9
|
+
Deno?: { env?: { get?: (name: string) => string | undefined } };
|
|
10
|
+
process?: { env?: Record<string, string | undefined> };
|
|
11
|
+
};
|
|
12
|
+
const value = global.process?.env?.[name] ?? global.Deno?.env?.get?.(name);
|
|
13
|
+
return value?.trim() || undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readable(credentials: Readonly<Record<string, CredentialSpec>>): ReadonlyArray<string> {
|
|
17
|
+
return Object.keys(credentials).filter(
|
|
18
|
+
(name) => credentials[name]!.variable && credentials[name]!.option !== false,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function envDefaults(
|
|
23
|
+
base: ResolvedOptions,
|
|
24
|
+
env: Readonly<Record<string, string>> = {},
|
|
25
|
+
credentials: Readonly<Record<string, CredentialSpec>> = {},
|
|
26
|
+
): ResolvedOptions {
|
|
27
|
+
const reads = new WeakMap<object, ResolvedOptions>();
|
|
28
|
+
const held = readable(credentials);
|
|
29
|
+
|
|
30
|
+
function resolve(): ResolvedOptions {
|
|
31
|
+
const read: ResolvedOptions = {};
|
|
32
|
+
for (const [option, variable] of Object.entries(env)) {
|
|
33
|
+
const value = readEnv(variable) ?? base[option];
|
|
34
|
+
if (value !== undefined) read[option] = value;
|
|
35
|
+
}
|
|
36
|
+
if (held.length) {
|
|
37
|
+
const tokens: Record<string, string | undefined> = {};
|
|
38
|
+
for (const name of held) tokens[name] = readEnv(credentials[name]!.variable!);
|
|
39
|
+
Object.assign(read, tokens);
|
|
40
|
+
const environment = impliedEnvironment(tokens, credentials) ?? base.environment;
|
|
41
|
+
if (environment !== undefined) read.environment = environment;
|
|
42
|
+
}
|
|
43
|
+
return read;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolvedFor(holder: object): ResolvedOptions {
|
|
47
|
+
let read = reads.get(holder);
|
|
48
|
+
if (!read) {
|
|
49
|
+
read = resolve();
|
|
50
|
+
reads.set(holder, read);
|
|
51
|
+
}
|
|
52
|
+
return read;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const decided = [...Object.keys(env), ...held, ...(held.length ? ['environment'] : [])];
|
|
56
|
+
|
|
57
|
+
const defaults = { ...base };
|
|
58
|
+
for (const key of decided) {
|
|
59
|
+
Object.defineProperty(defaults, key, {
|
|
60
|
+
configurable: true,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get() {
|
|
63
|
+
return resolvedFor(this as object)[key];
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return defaults;
|
|
68
|
+
}
|