@bentonow/bento-node-sdk 1.0.4 → 1.0.5
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 +256 -100
- package/dist/index.js +101 -68
- package/dist/sdk/batch/index.d.ts +1 -1
- package/dist/sdk/broadcasts/index.d.ts +2 -1
- package/dist/sdk/broadcasts/types.d.ts +4 -0
- package/dist/sdk/client/errors.d.ts +3 -0
- package/dist/sdk/client/index.d.ts +23 -3
- package/dist/sdk/interfaces.d.ts +4 -0
- package/dist/sdk/sequences/index.d.ts +2 -2
- package/dist/sdk/workflows/index.d.ts +2 -2
- package/dist/versions/v1/index.d.ts +7 -3
- package/package.json +1 -1
- package/src/sdk/batch/index.ts +10 -5
- package/src/sdk/broadcasts/index.ts +8 -7
- package/src/sdk/broadcasts/types.ts +5 -1
- package/src/sdk/client/errors.ts +7 -0
- package/src/sdk/client/index.ts +122 -49
- package/src/sdk/interfaces.ts +4 -0
- package/src/sdk/sequences/index.ts +4 -4
- package/src/sdk/workflows/index.ts +4 -4
- package/src/versions/v1/index.ts +16 -10
package/src/sdk/client/index.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import fetch from 'cross-fetch';
|
|
2
2
|
import type { AnalyticsOptions, AuthenticationOptions } from '../interfaces';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
NotAuthorizedError,
|
|
5
|
+
RateLimitedError,
|
|
6
|
+
AuthorNotAuthorizedError,
|
|
7
|
+
RequestTimeoutError,
|
|
8
|
+
} from './errors';
|
|
9
|
+
|
|
10
|
+
interface RequestOptions {
|
|
11
|
+
timeout?: number | null;
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
function encodeBase64(str: string): string {
|
|
6
15
|
if (typeof btoa === 'function') {
|
|
@@ -29,17 +38,20 @@ function encodeBase64(str: string): string {
|
|
|
29
38
|
return output;
|
|
30
39
|
}
|
|
31
40
|
}
|
|
41
|
+
|
|
32
42
|
export class BentoClient {
|
|
33
43
|
private readonly _headers: HeadersInit = {};
|
|
34
44
|
private readonly _baseUrl: string = 'https://app.bentonow.com/api/v1';
|
|
35
45
|
private readonly _siteUuid: string = '';
|
|
36
46
|
private readonly _logErrors: boolean = false;
|
|
47
|
+
private readonly _timeout: number = 30000; // 30 seconds default
|
|
37
48
|
|
|
38
49
|
constructor(options: AnalyticsOptions) {
|
|
39
50
|
this._baseUrl = options.clientOptions?.baseUrl || this._baseUrl;
|
|
40
51
|
this._siteUuid = options.siteUuid;
|
|
41
52
|
this._headers = this._extractHeaders(options.authentication, options.siteUuid);
|
|
42
53
|
this._logErrors = options.logErrors || false;
|
|
54
|
+
this._timeout = options.clientOptions?.timeout ?? this._timeout;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
/**
|
|
@@ -50,24 +62,26 @@ export class BentoClient {
|
|
|
50
62
|
* @param payload object
|
|
51
63
|
* @returns Promise\<T\>
|
|
52
64
|
* */
|
|
53
|
-
public get<T>(
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
public async get<T>(
|
|
66
|
+
endpoint: string,
|
|
67
|
+
payload: Record<string, unknown> = {},
|
|
68
|
+
requestOptions: RequestOptions = {}
|
|
69
|
+
): Promise<T> {
|
|
70
|
+
const queryParameters = this._getQueryParameters(payload);
|
|
71
|
+
const url = `${this._baseUrl}${endpoint}?${queryParameters}`;
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
const timeoutMs =
|
|
74
|
+
requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
|
|
75
|
+
const response = await this._fetchWithTimeout(
|
|
76
|
+
url,
|
|
77
|
+
{
|
|
58
78
|
method: 'GET',
|
|
59
79
|
headers: this._headers,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw await this._getErrorForResponse(result);
|
|
67
|
-
})
|
|
68
|
-
.then((data) => resolve(data))
|
|
69
|
-
.catch((error) => reject(error));
|
|
70
|
-
});
|
|
80
|
+
},
|
|
81
|
+
timeoutMs
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return this._handleResponse<T>(response);
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
/**
|
|
@@ -78,28 +92,30 @@ export class BentoClient {
|
|
|
78
92
|
* @param payload object
|
|
79
93
|
* @returns Promise\<T\>
|
|
80
94
|
* */
|
|
81
|
-
public post<T>(
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
public async post<T>(
|
|
96
|
+
endpoint: string,
|
|
97
|
+
payload: Record<string, unknown> = {},
|
|
98
|
+
requestOptions: RequestOptions = {}
|
|
99
|
+
): Promise<T> {
|
|
100
|
+
const body = this._getBody(payload);
|
|
101
|
+
const url = `${this._baseUrl}${endpoint}`;
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
const timeoutMs =
|
|
104
|
+
requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
|
|
105
|
+
const response = await this._fetchWithTimeout(
|
|
106
|
+
url,
|
|
107
|
+
{
|
|
86
108
|
method: 'POST',
|
|
87
109
|
headers: {
|
|
88
110
|
...this._headers,
|
|
89
111
|
'Content-Type': 'application/json',
|
|
90
112
|
},
|
|
91
113
|
body,
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw await this._getErrorForResponse(result);
|
|
99
|
-
})
|
|
100
|
-
.then((data) => resolve(data))
|
|
101
|
-
.catch((error) => reject(error));
|
|
102
|
-
});
|
|
114
|
+
},
|
|
115
|
+
timeoutMs
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return this._handleResponse<T>(response);
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
/**
|
|
@@ -110,28 +126,85 @@ export class BentoClient {
|
|
|
110
126
|
* @param payload object
|
|
111
127
|
* @returns Promise\<T\>
|
|
112
128
|
* */
|
|
113
|
-
public patch<T>(
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
public async patch<T>(
|
|
130
|
+
endpoint: string,
|
|
131
|
+
payload: Record<string, unknown> = {},
|
|
132
|
+
requestOptions: RequestOptions = {}
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
const body = this._getBody(payload);
|
|
135
|
+
const url = `${this._baseUrl}${endpoint}`;
|
|
116
136
|
|
|
117
|
-
|
|
137
|
+
const timeoutMs =
|
|
138
|
+
requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
|
|
139
|
+
const response = await this._fetchWithTimeout(
|
|
140
|
+
url,
|
|
141
|
+
{
|
|
118
142
|
method: 'PATCH',
|
|
119
143
|
headers: {
|
|
120
144
|
...this._headers,
|
|
121
145
|
'Content-Type': 'application/json',
|
|
122
146
|
},
|
|
123
147
|
body,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
},
|
|
149
|
+
timeoutMs
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return this._handleResponse<T>(response);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Performs a fetch request with a configurable timeout.
|
|
157
|
+
*
|
|
158
|
+
* @param url The URL to fetch
|
|
159
|
+
* @param options Fetch options
|
|
160
|
+
* @returns Promise<Response>
|
|
161
|
+
*/
|
|
162
|
+
private async _fetchWithTimeout(
|
|
163
|
+
url: string,
|
|
164
|
+
options: RequestInit,
|
|
165
|
+
timeout: number | null
|
|
166
|
+
): Promise<Response> {
|
|
167
|
+
if (timeout === null) {
|
|
168
|
+
return fetch(url, options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const controller = new AbortController();
|
|
172
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
...options,
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
return response;
|
|
180
|
+
} catch (error: unknown) {
|
|
181
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
182
|
+
throw new RequestTimeoutError(`Request timed out after ${timeout}ms`);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(timeoutId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handles the response from a fetch request, parsing JSON or throwing appropriate errors.
|
|
192
|
+
*
|
|
193
|
+
* @param response The fetch Response object
|
|
194
|
+
* @returns Promise<T> The parsed response data
|
|
195
|
+
*/
|
|
196
|
+
private async _handleResponse<T>(response: Response): Promise<T> {
|
|
197
|
+
if (this._isSuccessfulStatus(response.status)) {
|
|
198
|
+
try {
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
return data as T;
|
|
201
|
+
} catch {
|
|
202
|
+
// If JSON parsing fails on a successful response, throw a descriptive error
|
|
203
|
+
throw new Error(`[${response.status}] - Invalid JSON response from server`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
throw await this._getErrorForResponse(response);
|
|
135
208
|
}
|
|
136
209
|
|
|
137
210
|
/**
|
|
@@ -182,7 +255,7 @@ export class BentoClient {
|
|
|
182
255
|
|
|
183
256
|
const queryParameters = new URLSearchParams();
|
|
184
257
|
for (const [key, value] of Object.entries(body)) {
|
|
185
|
-
queryParameters.append(key, value);
|
|
258
|
+
queryParameters.append(key, String(value));
|
|
186
259
|
}
|
|
187
260
|
|
|
188
261
|
return queryParameters.toString();
|
|
@@ -216,7 +289,7 @@ export class BentoClient {
|
|
|
216
289
|
|
|
217
290
|
const contentType = response.headers.get('Content-Type');
|
|
218
291
|
let responseMessage = '';
|
|
219
|
-
let json:
|
|
292
|
+
let json: Record<string, unknown> | null = null;
|
|
220
293
|
|
|
221
294
|
// Try to parse the response body based on content type
|
|
222
295
|
try {
|
|
@@ -244,7 +317,7 @@ export class BentoClient {
|
|
|
244
317
|
|
|
245
318
|
// Check for author not authorized error in JSON response
|
|
246
319
|
if (json && json.error === 'Author not authorized to send on this account') {
|
|
247
|
-
return new AuthorNotAuthorizedError(json.error);
|
|
320
|
+
return new AuthorNotAuthorizedError(json.error as string);
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
// If we have JSON but no specific error match, use the JSON string
|
package/src/sdk/interfaces.ts
CHANGED
|
@@ -10,12 +10,12 @@ export class BentoSequences {
|
|
|
10
10
|
/**
|
|
11
11
|
* Returns all of the sequences for the site, including their email templates.
|
|
12
12
|
*
|
|
13
|
-
* @returns Promise\<Sequence[]
|
|
13
|
+
* @returns Promise\<Sequence[]\>
|
|
14
14
|
*/
|
|
15
|
-
public async getSequences(): Promise<Sequence[]
|
|
15
|
+
public async getSequences(): Promise<Sequence[]> {
|
|
16
16
|
const result = await this._client.get<DataResponse<Sequence[]>>(this._url);
|
|
17
17
|
|
|
18
|
-
if (Object.keys(result).length === 0
|
|
19
|
-
return result.data;
|
|
18
|
+
if (!result || Object.keys(result).length === 0) return [];
|
|
19
|
+
return result.data ?? [];
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -10,12 +10,12 @@ export class BentoWorkflows {
|
|
|
10
10
|
/**
|
|
11
11
|
* Returns all of the workflows for the site, including their email templates.
|
|
12
12
|
*
|
|
13
|
-
* @returns Promise\<Workflow[]
|
|
13
|
+
* @returns Promise\<Workflow[]\>
|
|
14
14
|
*/
|
|
15
|
-
public async getWorkflows(): Promise<Workflow[]
|
|
15
|
+
public async getWorkflows(): Promise<Workflow[]> {
|
|
16
16
|
const result = await this._client.get<DataResponse<Workflow[]>>(this._url);
|
|
17
17
|
|
|
18
|
-
if (Object.keys(result).length === 0
|
|
19
|
-
return result.data;
|
|
18
|
+
if (!result || Object.keys(result).length === 0) return [];
|
|
19
|
+
return result.data ?? [];
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/versions/v1/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
} from './types';
|
|
24
24
|
import { BentoBroadcasts } from '../../sdk/broadcasts';
|
|
25
25
|
import { BentoStats } from '../../sdk/stats';
|
|
26
|
-
import { Subscriber } from '../../sdk/subscribers/types';
|
|
26
|
+
import type { Subscriber } from '../../sdk/subscribers/types';
|
|
27
27
|
|
|
28
28
|
export class BentoAPIV1<S = { [key: string]: unknown }, E extends string = '$custom'> {
|
|
29
29
|
private readonly _client: BentoClient;
|
|
@@ -255,9 +255,13 @@ export class BentoAPIV1<S = { [key: string]: unknown }, E extends string = '$cus
|
|
|
255
255
|
* Upserts a subscriber in Bento. If the subscriber exists, their data will be updated.
|
|
256
256
|
* If they don't exist, they will be created with the provided data.
|
|
257
257
|
*
|
|
258
|
+
* This method still relies on the batch import queue (which can take 1-5 minutes to
|
|
259
|
+
* finish processing), but it automatically attempts to fetch and return the subscriber
|
|
260
|
+
* record after the import has been queued.
|
|
261
|
+
*
|
|
258
262
|
* @example
|
|
259
263
|
* ```typescript
|
|
260
|
-
* await analytics.V1.upsertSubscriber({
|
|
264
|
+
* const subscriber = await analytics.V1.upsertSubscriber({
|
|
261
265
|
* email: 'user@example.com',
|
|
262
266
|
* fields: {
|
|
263
267
|
* firstName: 'John',
|
|
@@ -269,7 +273,7 @@ export class BentoAPIV1<S = { [key: string]: unknown }, E extends string = '$cus
|
|
|
269
273
|
* ```
|
|
270
274
|
*
|
|
271
275
|
* @param parameters Object containing subscriber data including email, fields, and tags
|
|
272
|
-
* @returns Promise<Subscriber<S
|
|
276
|
+
* @returns Promise<Subscriber<S> | null> The created or updated subscriber
|
|
273
277
|
*/
|
|
274
278
|
public async upsertSubscriber(
|
|
275
279
|
parameters: Omit<AddSubscriberParameters<S>, 'date'> & {
|
|
@@ -278,16 +282,18 @@ export class BentoAPIV1<S = { [key: string]: unknown }, E extends string = '$cus
|
|
|
278
282
|
}
|
|
279
283
|
): Promise<Subscriber<S> | null> {
|
|
280
284
|
await this.Batch.importSubscribers({
|
|
281
|
-
subscribers: [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
subscribers: [
|
|
286
|
+
{
|
|
287
|
+
email: parameters.email,
|
|
288
|
+
...parameters.fields,
|
|
289
|
+
...(parameters.tags && { tags: parameters.tags }),
|
|
290
|
+
...(parameters.remove_tags && { remove_tags: parameters.remove_tags }),
|
|
291
|
+
} as { email: string } & Partial<S>,
|
|
292
|
+
],
|
|
287
293
|
});
|
|
288
294
|
|
|
289
295
|
return this.Subscribers.getSubscribers({
|
|
290
|
-
email: parameters.email
|
|
296
|
+
email: parameters.email,
|
|
291
297
|
});
|
|
292
298
|
}
|
|
293
299
|
}
|