@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/dist/index.js CHANGED
@@ -626,7 +626,7 @@ class BentoBatch {
626
626
  }
627
627
  const result = await this._client.post(`${this._url}/subscribers`, {
628
628
  subscribers: parameters.subscribers
629
- });
629
+ }, { timeout: null });
630
630
  return result.results;
631
631
  }
632
632
  async importEvents(parameters) {
@@ -638,7 +638,7 @@ class BentoBatch {
638
638
  }
639
639
  const result = await this._client.post(`${this._url}/events`, {
640
640
  events: parameters.events
641
- });
641
+ }, { timeout: null });
642
642
  return result.results;
643
643
  }
644
644
  async sendTransactionalEmails(parameters) {
@@ -679,6 +679,13 @@ class AuthorNotAuthorizedError extends Error {
679
679
  }
680
680
  }
681
681
 
682
+ class RequestTimeoutError extends Error {
683
+ constructor(message = "Request timed out") {
684
+ super(message);
685
+ this.name = "RequestTimeoutError";
686
+ }
687
+ }
688
+
682
689
  // src/sdk/client/index.ts
683
690
  function encodeBase64(str) {
684
691
  if (typeof btoa === "function") {
@@ -704,61 +711,83 @@ class BentoClient {
704
711
  _baseUrl = "https://app.bentonow.com/api/v1";
705
712
  _siteUuid = "";
706
713
  _logErrors = false;
714
+ _timeout = 30000;
707
715
  constructor(options) {
708
716
  this._baseUrl = options.clientOptions?.baseUrl || this._baseUrl;
709
717
  this._siteUuid = options.siteUuid;
710
718
  this._headers = this._extractHeaders(options.authentication, options.siteUuid);
711
719
  this._logErrors = options.logErrors || false;
720
+ this._timeout = options.clientOptions?.timeout ?? this._timeout;
721
+ }
722
+ async get(endpoint, payload = {}, requestOptions = {}) {
723
+ const queryParameters = this._getQueryParameters(payload);
724
+ const url = `${this._baseUrl}${endpoint}?${queryParameters}`;
725
+ const timeoutMs = requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
726
+ const response = await this._fetchWithTimeout(url, {
727
+ method: "GET",
728
+ headers: this._headers
729
+ }, timeoutMs);
730
+ return this._handleResponse(response);
731
+ }
732
+ async post(endpoint, payload = {}, requestOptions = {}) {
733
+ const body = this._getBody(payload);
734
+ const url = `${this._baseUrl}${endpoint}`;
735
+ const timeoutMs = requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
736
+ const response = await this._fetchWithTimeout(url, {
737
+ method: "POST",
738
+ headers: {
739
+ ...this._headers,
740
+ "Content-Type": "application/json"
741
+ },
742
+ body
743
+ }, timeoutMs);
744
+ return this._handleResponse(response);
745
+ }
746
+ async patch(endpoint, payload = {}, requestOptions = {}) {
747
+ const body = this._getBody(payload);
748
+ const url = `${this._baseUrl}${endpoint}`;
749
+ const timeoutMs = requestOptions.timeout === undefined ? this._timeout : requestOptions.timeout;
750
+ const response = await this._fetchWithTimeout(url, {
751
+ method: "PATCH",
752
+ headers: {
753
+ ...this._headers,
754
+ "Content-Type": "application/json"
755
+ },
756
+ body
757
+ }, timeoutMs);
758
+ return this._handleResponse(response);
759
+ }
760
+ async _fetchWithTimeout(url, options, timeout) {
761
+ if (timeout === null) {
762
+ return import_cross_fetch.default(url, options);
763
+ }
764
+ const controller = new AbortController;
765
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
766
+ try {
767
+ const response = await import_cross_fetch.default(url, {
768
+ ...options,
769
+ signal: controller.signal
770
+ });
771
+ return response;
772
+ } catch (error) {
773
+ if (error instanceof Error && error.name === "AbortError") {
774
+ throw new RequestTimeoutError(`Request timed out after ${timeout}ms`);
775
+ }
776
+ throw error;
777
+ } finally {
778
+ clearTimeout(timeoutId);
779
+ }
712
780
  }
713
- get(endpoint, payload = {}) {
714
- return new Promise((resolve, reject) => {
715
- const queryParameters = this._getQueryParameters(payload);
716
- import_cross_fetch.default(`${this._baseUrl}${endpoint}?${queryParameters}`, {
717
- method: "GET",
718
- headers: this._headers
719
- }).then(async (result) => {
720
- if (this._isSuccessfulStatus(result.status)) {
721
- return result.json();
722
- }
723
- throw await this._getErrorForResponse(result);
724
- }).then((data) => resolve(data)).catch((error) => reject(error));
725
- });
726
- }
727
- post(endpoint, payload = {}) {
728
- return new Promise((resolve, reject) => {
729
- const body = this._getBody(payload);
730
- import_cross_fetch.default(`${this._baseUrl}${endpoint}`, {
731
- method: "POST",
732
- headers: {
733
- ...this._headers,
734
- "Content-Type": "application/json"
735
- },
736
- body
737
- }).then(async (result) => {
738
- if (this._isSuccessfulStatus(result.status)) {
739
- return result.json();
740
- }
741
- throw await this._getErrorForResponse(result);
742
- }).then((data) => resolve(data)).catch((error) => reject(error));
743
- });
744
- }
745
- patch(endpoint, payload = {}) {
746
- return new Promise((resolve, reject) => {
747
- const body = this._getBody(payload);
748
- import_cross_fetch.default(`${this._baseUrl}${endpoint}`, {
749
- method: "PATCH",
750
- headers: {
751
- ...this._headers,
752
- "Content-Type": "application/json"
753
- },
754
- body
755
- }).then(async (result) => {
756
- if (this._isSuccessfulStatus(result.status)) {
757
- return result.json();
758
- }
759
- throw await this._getErrorForResponse(result);
760
- }).then((data) => resolve(data)).catch((error) => reject(error));
761
- });
781
+ async _handleResponse(response) {
782
+ if (this._isSuccessfulStatus(response.status)) {
783
+ try {
784
+ const data = await response.json();
785
+ return data;
786
+ } catch {
787
+ throw new Error(`[${response.status}] - Invalid JSON response from server`);
788
+ }
789
+ }
790
+ throw await this._getErrorForResponse(response);
762
791
  }
763
792
  _extractHeaders(authentication, siteUuid) {
764
793
  const authenticationKey = encodeBase64(`${authentication.publishableKey}:${authentication.secretKey}`);
@@ -780,7 +809,7 @@ class BentoClient {
780
809
  };
781
810
  const queryParameters = new URLSearchParams;
782
811
  for (const [key, value] of Object.entries(body)) {
783
- queryParameters.append(key, value);
812
+ queryParameters.append(key, String(value));
784
813
  }
785
814
  return queryParameters.toString();
786
815
  }
@@ -1034,9 +1063,9 @@ class BentoSequences {
1034
1063
  }
1035
1064
  async getSequences() {
1036
1065
  const result = await this._client.get(this._url);
1037
- if (Object.keys(result).length === 0 || !result.data)
1038
- return null;
1039
- return result.data;
1066
+ if (!result || Object.keys(result).length === 0)
1067
+ return [];
1068
+ return result.data ?? [];
1040
1069
  }
1041
1070
  }
1042
1071
  // src/sdk/subscribers/index.ts
@@ -1092,16 +1121,17 @@ class BentoWorkflows {
1092
1121
  }
1093
1122
  async getWorkflows() {
1094
1123
  const result = await this._client.get(this._url);
1095
- if (Object.keys(result).length === 0 || !result.data)
1096
- return null;
1097
- return result.data;
1124
+ if (!result || Object.keys(result).length === 0)
1125
+ return [];
1126
+ return result.data ?? [];
1098
1127
  }
1099
1128
  }
1100
1129
  // src/sdk/broadcasts/index.ts
1101
1130
  class BentoBroadcasts {
1102
1131
  _client;
1103
- _url = "/broadcasts";
1104
- _emailsUrl = "/emails";
1132
+ _fetchUrl = "/fetch/broadcasts";
1133
+ _batchUrl = "/batch/broadcasts";
1134
+ _emailsUrl = "/batch/emails";
1105
1135
  constructor(_client) {
1106
1136
  this._client = _client;
1107
1137
  }
@@ -1112,11 +1142,11 @@ class BentoBroadcasts {
1112
1142
  return result.results;
1113
1143
  }
1114
1144
  async getBroadcasts() {
1115
- const result = await this._client.get(this._url);
1145
+ const result = await this._client.get(this._fetchUrl);
1116
1146
  return result.data ?? [];
1117
1147
  }
1118
1148
  async createBroadcast(broadcasts) {
1119
- const result = await this._client.post(this._url, {
1149
+ const result = await this._client.post(this._batchUrl, {
1120
1150
  broadcasts
1121
1151
  });
1122
1152
  return result.data ?? [];
@@ -1248,12 +1278,14 @@ class BentoAPIV1 {
1248
1278
  }
1249
1279
  async upsertSubscriber(parameters) {
1250
1280
  await this.Batch.importSubscribers({
1251
- subscribers: [{
1252
- email: parameters.email,
1253
- ...parameters.fields,
1254
- ...parameters.tags && { tags: parameters.tags },
1255
- ...parameters.remove_tags && { remove_tags: parameters.remove_tags }
1256
- }]
1281
+ subscribers: [
1282
+ {
1283
+ email: parameters.email,
1284
+ ...parameters.fields,
1285
+ ...parameters.tags && { tags: parameters.tags },
1286
+ ...parameters.remove_tags && { remove_tags: parameters.remove_tags }
1287
+ }
1288
+ ]
1257
1289
  });
1258
1290
  return this.Subscribers.getSubscribers({
1259
1291
  email: parameters.email
@@ -1269,6 +1301,7 @@ class Analytics {
1269
1301
  }
1270
1302
  }
1271
1303
  export {
1304
+ RequestTimeoutError,
1272
1305
  RateLimitedError,
1273
1306
  NotAuthorizedError,
1274
1307
  AuthorNotAuthorizedError,
@@ -44,7 +44,7 @@ export declare class BentoBatch<S, E extends string> {
44
44
  * Each email must have a `to` address, a `from` address, a `subject`, an `html_body`
45
45
  * and `transactional: true`.
46
46
  * In addition you can add a `personalizations` object to provide
47
- * liquid tsags that will be injected into the email.
47
+ * liquid tags that will be injected into the email.
48
48
  *
49
49
  * Returns the number of events that were imported.
50
50
  *
@@ -2,7 +2,8 @@ import type { BentoClient } from '../client';
2
2
  import type { Broadcast, CreateBroadcastInput, EmailData } from './types';
3
3
  export declare class BentoBroadcasts {
4
4
  private readonly _client;
5
- private readonly _url;
5
+ private readonly _fetchUrl;
6
+ private readonly _batchUrl;
6
7
  private readonly _emailsUrl;
7
8
  constructor(_client: BentoClient);
8
9
  /**
@@ -18,6 +18,10 @@ export type BroadcastAttributes = {
18
18
  };
19
19
  export type Broadcast = BaseEntity<BroadcastAttributes>;
20
20
  export type CreateBroadcastInput = Omit<BroadcastAttributes, 'created_at'>;
21
+ /**
22
+ * Email data for transactional emails.
23
+ * Note: This is the same structure as TransactionalEmail in batch/types.ts
24
+ */
21
25
  export type EmailData = {
22
26
  to: string;
23
27
  from: string;
@@ -7,3 +7,6 @@ export declare class RateLimitedError extends Error {
7
7
  export declare class AuthorNotAuthorizedError extends Error {
8
8
  constructor(message?: string);
9
9
  }
10
+ export declare class RequestTimeoutError extends Error {
11
+ constructor(message?: string);
12
+ }
@@ -1,9 +1,13 @@
1
1
  import type { AnalyticsOptions } from '../interfaces';
2
+ interface RequestOptions {
3
+ timeout?: number | null;
4
+ }
2
5
  export declare class BentoClient {
3
6
  private readonly _headers;
4
7
  private readonly _baseUrl;
5
8
  private readonly _siteUuid;
6
9
  private readonly _logErrors;
10
+ private readonly _timeout;
7
11
  constructor(options: AnalyticsOptions);
8
12
  /**
9
13
  * Wraps a GET request to the Bento API and automatically adds the required
@@ -13,7 +17,7 @@ export declare class BentoClient {
13
17
  * @param payload object
14
18
  * @returns Promise\<T\>
15
19
  * */
16
- get<T>(endpoint: string, payload?: Record<string, unknown>): Promise<T>;
20
+ get<T>(endpoint: string, payload?: Record<string, unknown>, requestOptions?: RequestOptions): Promise<T>;
17
21
  /**
18
22
  * Wraps a POST request to the Bento API and automatically adds the required
19
23
  * headers.
@@ -22,7 +26,7 @@ export declare class BentoClient {
22
26
  * @param payload object
23
27
  * @returns Promise\<T\>
24
28
  * */
25
- post<T>(endpoint: string, payload?: Record<string, unknown>): Promise<T>;
29
+ post<T>(endpoint: string, payload?: Record<string, unknown>, requestOptions?: RequestOptions): Promise<T>;
26
30
  /**
27
31
  * Wraps a PATCH request to the Bento API and automatically adds the required
28
32
  * headers.
@@ -31,7 +35,22 @@ export declare class BentoClient {
31
35
  * @param payload object
32
36
  * @returns Promise\<T\>
33
37
  * */
34
- patch<T>(endpoint: string, payload?: Record<string, unknown>): Promise<T>;
38
+ patch<T>(endpoint: string, payload?: Record<string, unknown>, requestOptions?: RequestOptions): Promise<T>;
39
+ /**
40
+ * Performs a fetch request with a configurable timeout.
41
+ *
42
+ * @param url The URL to fetch
43
+ * @param options Fetch options
44
+ * @returns Promise<Response>
45
+ */
46
+ private _fetchWithTimeout;
47
+ /**
48
+ * Handles the response from a fetch request, parsing JSON or throwing appropriate errors.
49
+ *
50
+ * @param response The fetch Response object
51
+ * @returns Promise<T> The parsed response data
52
+ */
53
+ private _handleResponse;
35
54
  /**
36
55
  * Extracts the `publishableKey` and `secretKey` from the `authentication` options,
37
56
  * adds the `Authorization` header, and includes a `User-Agent` header with the site UUID.
@@ -72,3 +91,4 @@ export declare class BentoClient {
72
91
  */
73
92
  private _getErrorForResponse;
74
93
  }
94
+ export {};
@@ -10,4 +10,8 @@ export interface AuthenticationOptions {
10
10
  }
11
11
  export interface ClientOptions {
12
12
  baseUrl?: string;
13
+ /**
14
+ * Request timeout in milliseconds. Defaults to 30000 (30 seconds).
15
+ */
16
+ timeout?: number;
13
17
  }
@@ -7,7 +7,7 @@ export declare class BentoSequences {
7
7
  /**
8
8
  * Returns all of the sequences for the site, including their email templates.
9
9
  *
10
- * @returns Promise\<Sequence[] | null\>
10
+ * @returns Promise\<Sequence[]\>
11
11
  */
12
- getSequences(): Promise<Sequence[] | null>;
12
+ getSequences(): Promise<Sequence[]>;
13
13
  }
@@ -7,7 +7,7 @@ export declare class BentoWorkflows {
7
7
  /**
8
8
  * Returns all of the workflows for the site, including their email templates.
9
9
  *
10
- * @returns Promise\<Workflow[] | null\>
10
+ * @returns Promise\<Workflow[]\>
11
11
  */
12
- getWorkflows(): Promise<Workflow[] | null>;
12
+ getWorkflows(): Promise<Workflow[]>;
13
13
  }
@@ -3,7 +3,7 @@ import type { AnalyticsOptions } from '../../sdk/interfaces';
3
3
  import type { AddSubscriberParameters, RemoveSubscriberParameters, TagSubscriberParameters, TrackParameters, TrackPurchaseParameters, UpdateFieldsParameters } from './types';
4
4
  import { BentoBroadcasts } from '../../sdk/broadcasts';
5
5
  import { BentoStats } from '../../sdk/stats';
6
- import { Subscriber } from '../../sdk/subscribers/types';
6
+ import type { Subscriber } from '../../sdk/subscribers/types';
7
7
  export declare class BentoAPIV1<S = {
8
8
  [key: string]: unknown;
9
9
  }, E extends string = '$custom'> {
@@ -141,9 +141,13 @@ export declare class BentoAPIV1<S = {
141
141
  * Upserts a subscriber in Bento. If the subscriber exists, their data will be updated.
142
142
  * If they don't exist, they will be created with the provided data.
143
143
  *
144
+ * This method still relies on the batch import queue (which can take 1-5 minutes to
145
+ * finish processing), but it automatically attempts to fetch and return the subscriber
146
+ * record after the import has been queued.
147
+ *
144
148
  * @example
145
149
  * ```typescript
146
- * await analytics.V1.upsertSubscriber({
150
+ * const subscriber = await analytics.V1.upsertSubscriber({
147
151
  * email: 'user@example.com',
148
152
  * fields: {
149
153
  * firstName: 'John',
@@ -155,7 +159,7 @@ export declare class BentoAPIV1<S = {
155
159
  * ```
156
160
  *
157
161
  * @param parameters Object containing subscriber data including email, fields, and tags
158
- * @returns Promise<Subscriber<S>> The created or updated subscriber
162
+ * @returns Promise<Subscriber<S> | null> The created or updated subscriber
159
163
  */
160
164
  upsertSubscriber(parameters: Omit<AddSubscriberParameters<S>, 'date'> & {
161
165
  tags?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bentonow/bento-node-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "🍱 Bento Node.JS SDK and tracking library",
5
5
  "author": "Backpack Internet",
6
6
  "license": "MIT",
@@ -54,7 +54,8 @@ export class BentoBatch<S, E extends string> {
54
54
  `${this._url}/subscribers`,
55
55
  {
56
56
  subscribers: parameters.subscribers,
57
- }
57
+ },
58
+ { timeout: null }
58
59
  );
59
60
 
60
61
  return result.results;
@@ -80,9 +81,13 @@ export class BentoBatch<S, E extends string> {
80
81
  throw new TooManyEventsError(`You must send between 1 and 1,000 events.`);
81
82
  }
82
83
 
83
- const result = await this._client.post<BatchImportEventsResponse>(`${this._url}/events`, {
84
- events: parameters.events,
85
- });
84
+ const result = await this._client.post<BatchImportEventsResponse>(
85
+ `${this._url}/events`,
86
+ {
87
+ events: parameters.events,
88
+ },
89
+ { timeout: null }
90
+ );
86
91
 
87
92
  return result.results;
88
93
  }
@@ -94,7 +99,7 @@ export class BentoBatch<S, E extends string> {
94
99
  * Each email must have a `to` address, a `from` address, a `subject`, an `html_body`
95
100
  * and `transactional: true`.
96
101
  * In addition you can add a `personalizations` object to provide
97
- * liquid tsags that will be injected into the email.
102
+ * liquid tags that will be injected into the email.
98
103
  *
99
104
  * Returns the number of events that were imported.
100
105
  *
@@ -3,8 +3,9 @@ import type { DataResponse } from '../client/types';
3
3
  import type { Broadcast, CreateBroadcastInput, EmailData } from './types';
4
4
 
5
5
  export class BentoBroadcasts {
6
- private readonly _url = '/broadcasts';
7
- private readonly _emailsUrl = '/emails';
6
+ private readonly _fetchUrl = '/fetch/broadcasts';
7
+ private readonly _batchUrl = '/batch/broadcasts';
8
+ private readonly _emailsUrl = '/batch/emails';
8
9
 
9
10
  constructor(private readonly _client: BentoClient) {}
10
11
 
@@ -15,7 +16,7 @@ export class BentoBroadcasts {
15
16
  */
16
17
  public async createEmails(emails: EmailData[]): Promise<number> {
17
18
  const result = await this._client.post<{ results: number }>(this._emailsUrl, {
18
- emails
19
+ emails,
19
20
  });
20
21
  return result.results;
21
22
  }
@@ -25,7 +26,7 @@ export class BentoBroadcasts {
25
26
  * @returns Promise<Broadcast[]>
26
27
  */
27
28
  public async getBroadcasts(): Promise<Broadcast[]> {
28
- const result = await this._client.get<DataResponse<Broadcast[]>>(this._url);
29
+ const result = await this._client.get<DataResponse<Broadcast[]>>(this._fetchUrl);
29
30
  return result.data ?? [];
30
31
  }
31
32
 
@@ -35,9 +36,9 @@ export class BentoBroadcasts {
35
36
  * @returns Promise<Broadcast[]>
36
37
  */
37
38
  public async createBroadcast(broadcasts: CreateBroadcastInput[]): Promise<Broadcast[]> {
38
- const result = await this._client.post<DataResponse<Broadcast[]>>(this._url, {
39
- broadcasts
39
+ const result = await this._client.post<DataResponse<Broadcast[]>>(this._batchUrl, {
40
+ broadcasts,
40
41
  });
41
42
  return result.data ?? [];
42
43
  }
43
- }
44
+ }
@@ -24,6 +24,10 @@ export type Broadcast = BaseEntity<BroadcastAttributes>;
24
24
 
25
25
  export type CreateBroadcastInput = Omit<BroadcastAttributes, 'created_at'>;
26
26
 
27
+ /**
28
+ * Email data for transactional emails.
29
+ * Note: This is the same structure as TransactionalEmail in batch/types.ts
30
+ */
27
31
  export type EmailData = {
28
32
  to: string;
29
33
  from: string;
@@ -31,4 +35,4 @@ export type EmailData = {
31
35
  html_body: string;
32
36
  transactional: boolean;
33
37
  personalizations?: Record<string, string | number | boolean>;
34
- };
38
+ };
@@ -18,3 +18,10 @@ export class AuthorNotAuthorizedError extends Error {
18
18
  this.name = 'AuthorNotAuthorizedError';
19
19
  }
20
20
  }
21
+
22
+ export class RequestTimeoutError extends Error {
23
+ constructor(message = 'Request timed out') {
24
+ super(message);
25
+ this.name = 'RequestTimeoutError';
26
+ }
27
+ }