@or-sdk/contacts 3.2.2-beta.1771.0 → 3.2.2-beta.1775.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cjs/Contacts.js +1 -1
  2. package/dist/cjs/Contacts.js.map +1 -1
  3. package/dist/cjs/api/batchProcessApi.js +6 -0
  4. package/dist/cjs/api/batchProcessApi.js.map +1 -1
  5. package/dist/cjs/api/contactApi.js +143 -165
  6. package/dist/cjs/api/contactApi.js.map +1 -1
  7. package/dist/cjs/apiError.js +13 -1
  8. package/dist/cjs/apiError.js.map +1 -1
  9. package/dist/cjs/constants.js +2 -2
  10. package/dist/cjs/constants.js.map +1 -1
  11. package/dist/cjs/types.js.map +1 -1
  12. package/dist/cjs/utils.js +11 -1
  13. package/dist/cjs/utils.js.map +1 -1
  14. package/dist/esm/Contacts.js +1 -1
  15. package/dist/esm/Contacts.js.map +1 -1
  16. package/dist/esm/api/batchProcessApi.js +7 -1
  17. package/dist/esm/api/batchProcessApi.js.map +1 -1
  18. package/dist/esm/api/contactApi.js +82 -94
  19. package/dist/esm/api/contactApi.js.map +1 -1
  20. package/dist/esm/apiError.js +8 -0
  21. package/dist/esm/apiError.js.map +1 -1
  22. package/dist/esm/constants.js +2 -2
  23. package/dist/esm/constants.js.map +1 -1
  24. package/dist/esm/types.js.map +1 -1
  25. package/dist/esm/utils.js +9 -0
  26. package/dist/esm/utils.js.map +1 -1
  27. package/dist/types/api/batchProcessApi.d.ts +2 -1
  28. package/dist/types/api/batchProcessApi.d.ts.map +1 -1
  29. package/dist/types/api/contactApi.d.ts +7 -16
  30. package/dist/types/api/contactApi.d.ts.map +1 -1
  31. package/dist/types/apiError.d.ts +6 -0
  32. package/dist/types/apiError.d.ts.map +1 -1
  33. package/dist/types/constants.d.ts +2 -2
  34. package/dist/types/constants.d.ts.map +1 -1
  35. package/dist/types/types.d.ts +9 -5
  36. package/dist/types/types.d.ts.map +1 -1
  37. package/dist/types/utils.d.ts +1 -0
  38. package/dist/types/utils.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/Contacts.ts +1 -1
  41. package/src/api/batchProcessApi.ts +14 -5
  42. package/src/api/contactApi.ts +127 -127
  43. package/src/apiError.ts +11 -0
  44. package/src/constants.ts +8 -2
  45. package/src/types.ts +11 -11
  46. package/src/utils.ts +16 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BatchProcessResponseDto,
3
+ BatchProcessStatus,
3
4
  ContactParamsDto,
4
5
  ContactRequestDto,
5
6
  ContactResponseDto,
@@ -18,17 +19,15 @@ import {
18
19
  import { CalApiParams, List } from '@or-sdk/base';
19
20
  import { BaseApi } from './baseApi';
20
21
  import BatchProcessApi from './batchProcessApi';
21
- import ContactBookApi from './contactBookApi';
22
- import { getObjectSizeInBytes, chunkArrByMaxSize } from '../utils';
22
+ import { getObjectSizeInBytes, chunkArrByMaxSize, debouncePromise } from '../utils';
23
+ import { InitCreateBatchResponse, CreateContactsBatchResults } from '../types';
23
24
  import { REQUEST_PAYLOAD_MAX_BYTES, FAILED_REQUEST_REPEATS, CONTACTS_DELETE_MAX_AMOUNT } from '../constants';
24
- import { ApiError } from '../apiError';
25
- import { DeleteContactsData } from '../types';
25
+ import { CreateContactsBatchError } from '../apiError';
26
26
 
27
27
  export default class ContactApi extends BaseApi {
28
28
  constructor(
29
29
  protected readonly apiCall: <T>(params: CalApiParams) => Promise<T>,
30
- private batchProcessApi: BatchProcessApi,
31
- private bookServiceApi: ContactBookApi
30
+ private batchProcessApi: BatchProcessApi
32
31
  ) {
33
32
  super(apiCall);
34
33
  }
@@ -91,8 +90,8 @@ export default class ContactApi extends BaseApi {
91
90
  * @description Delete Contact
92
91
  * @param data
93
92
  */
94
- deleteMulti(data: DeleteContactsData): Promise<void> {
95
- if ((Array.isArray(data.ids) && data.ids.length >= CONTACTS_DELETE_MAX_AMOUNT) || data.all) {
93
+ deleteMulti(data: DeleteContactMultiParamsDto): Promise<void> {
94
+ if ((Array.isArray(data.ids) && data.ids.length > CONTACTS_DELETE_MAX_AMOUNT) || data.all) {
96
95
  return this.bulkDeleteContacts(data);
97
96
  }
98
97
  return this.apiCall({
@@ -102,45 +101,6 @@ export default class ContactApi extends BaseApi {
102
101
  });
103
102
  }
104
103
 
105
- /**
106
- * @description Delete all contacts from a book
107
- */
108
- async deleteContactsByBook(bookId: string): Promise<void> {
109
- const contacts = await this.listContact({ contact_book: bookId });
110
- return this.bulkDeleteContacts({
111
- ids: contacts.items.map(({ id }) => id),
112
- contact_book: bookId,
113
- });
114
- }
115
-
116
- /**
117
- * Initiates bulk of deleting all contacts from a certain book.
118
- * The only first batch is going to be executed and the only - its
119
- * process id is returned in the response.
120
- * Tracking of the batch status is possible with trackBatchProcess
121
- * from BatchProcessApi
122
- *
123
- * @see batchProcessApi.trackBatchProcess
124
- */
125
- async initDeleteBookContactsBulk(bookId: string) {
126
- const { contactsCount } = await this.bookServiceApi.getContactBook(bookId);
127
- const contacts = await this.listContact({
128
- contact_book: bookId,
129
- size: CONTACTS_DELETE_MAX_AMOUNT,
130
- });
131
-
132
- const batchProcess = await this.runSingleDeleteContactsBulk({
133
- ids: contacts.items.map(({ id }) => id),
134
- contact_book: bookId,
135
- });
136
-
137
- return {
138
- batchId: batchProcess.id,
139
- totalBatches: Math.floor(contactsCount / 50),
140
- firstBatchSize: CONTACTS_DELETE_MAX_AMOUNT,
141
- };
142
- }
143
-
144
104
  /**
145
105
  * @description Create Contact
146
106
  * @param data
@@ -155,18 +115,61 @@ export default class ContactApi extends BaseApi {
155
115
 
156
116
  /**
157
117
  * @description Create Contacts either in single or in multi batch(es), depending on payload size
158
- * @param data
159
118
  */
160
- async bulkCreateContacts(data: CreateMultipleContactsDto): Promise<ContactResponseDto[]> {
119
+ async bulkCreateContacts(data: CreateMultipleContactsDto): Promise<string[]> {
120
+ // eslint-disable-next-line no-console
121
+ console.log('contacts: ', data.contacts.length);
122
+ const { contacts, ...rest } = data;
123
+ const contactsMaxSize = REQUEST_PAYLOAD_MAX_BYTES - getObjectSizeInBytes({ ...rest });
124
+ const contactsChunks = chunkArrByMaxSize(contacts, contactsMaxSize);
125
+
126
+ contactsChunks.forEach((chunk, idx) => {
127
+ // eslint-disable-next-line no-console
128
+ console.log('size of ', idx, ' chunk', ' is ', getObjectSizeInBytes(chunk));
129
+ });
130
+ // eslint-disable-next-line no-console
131
+ console.log('chunks: ', contactsChunks.length);
132
+ const results = contactsChunks.length === 1
133
+ ? [await this.createContactsInSingleBatch(data)]
134
+ : await this.createContactsInMultiBatches(contactsChunks, rest);
135
+
136
+ return results.reduce<string[]>((acc, { contactsIds }) => ([...acc, ...contactsIds]), []);
137
+ }
138
+
139
+ /**
140
+ * Breaks the contacts to chunks of maximum allowed size (200 KB) and launches create batch
141
+ * only for the first chunk.
142
+ * Returns batch id in response by which the batch can be tracked using batch process API.
143
+ * Note: since create contact batch is a very expensive operations from terms of consuming
144
+ * DB connections, it is highly recommended to check amount of pending (running) batches
145
+ * prior to this method execution, and if the amount is greater of 2 the batch initiation
146
+ * should be deferred.
147
+ *
148
+ * @see batchProcessApi.trackBatchProcess
149
+ * @see batchProcessApi.getPendingBatchProcesses
150
+ */
151
+ async initCreateBatch(data: CreateMultipleContactsDto): Promise<InitCreateBatchResponse> {
161
152
  const { contacts, ...rest } = data;
162
153
  const contactsMaxSize = REQUEST_PAYLOAD_MAX_BYTES - getObjectSizeInBytes({ ...rest });
163
154
  const contactsChunks = chunkArrByMaxSize(contacts, contactsMaxSize);
164
155
 
165
- return contactsChunks.length === 1
166
- ? this.createContactsInSingleBatch(data)
167
- : this.createContactsInMultiBatches(contactsChunks, rest);
156
+ const batchProcess = await this.apiCall<BatchProcessResponseDto>({
157
+ method: 'POST',
158
+ route: `${this.apiBasePath}/bulk`,
159
+ data: {
160
+ contacts: contactsChunks[0],
161
+ ...rest,
162
+ },
163
+ });
164
+
165
+ return {
166
+ totalChunks: contactsChunks.length,
167
+ firstChunkSize: contactsChunks[0].length,
168
+ batchId: batchProcess.id,
169
+ };
168
170
  }
169
171
 
172
+
170
173
  /**
171
174
  * @description Merge two Contacts into one
172
175
  * @param id Contact id TO which the data will be merged
@@ -230,102 +233,99 @@ export default class ContactApi extends BaseApi {
230
233
  });
231
234
  }
232
235
 
233
- private async getSafelyContactsList(
234
- contactIds: string[],
235
- bookId: string | undefined,
236
- repeats?: number
237
- ): Promise<List<ContactResponseDto>> {
238
- const contacts = await this.listContact({
239
- contactIds,
240
- ...(bookId && { contact_book: bookId }),
241
- }).catch((e) => {
242
- repeats = repeats || 0;
243
- if (repeats < FAILED_REQUEST_REPEATS) {
244
- return this.getSafelyContactsList(contactIds, bookId, repeats + 1);
245
- }
246
- throw new Error(e);
247
- });
248
- return contacts;
249
- }
250
-
251
- private async bulkDeleteContacts(data: DeleteContactsData): Promise<void> {
252
- const { ids, contact_book, all } = data;
253
- if (all) {
254
- if (!contact_book) {
255
- throw new ApiError(400, 'contact_book should be provided in case if "all" is true');
256
- }
257
- return await this.deleteContactsByBook(contact_book);
258
- }
259
- if (!ids) {
260
- throw new ApiError(400, 'Provide either "contact_book" or "all"');
261
- }
262
- const batchSize = CONTACTS_DELETE_MAX_AMOUNT;
263
- for (let i = 0;i < ids.length;i += batchSize) {
264
- const chunk = ids.slice(i, i + batchSize);
265
- await this.runSingleDeleteContactsBulk({
266
- ids: chunk,
267
- contact_book,
268
- });
269
- }
270
- }
271
-
272
- private async runSingleDeleteContactsBulk(
273
- data: DeleteContactMultiParamsDto,
274
- withPoling = true
275
- ): Promise<BatchProcessResponseDto> {
236
+ private async bulkDeleteContacts(data: DeleteContactMultiParamsDto): Promise<void> {
276
237
  const batchProcess = await this.apiCall<BatchProcessResponseDto>({
277
238
  method: 'DELETE',
278
239
  route: `${this.apiBasePath}/bulk`,
279
240
  data,
280
241
  });
281
- if (withPoling) {
282
- const result = await this.polling(batchProcess.id);
283
- return result;
242
+ await this.polling(batchProcess.id);
243
+ }
244
+
245
+ private async createContactsInMultiBatches(
246
+ contactsChunks: ContactRequestDto[][],
247
+ data: Omit<CreateMultipleContactsDto, 'contacts'>
248
+ ): Promise<CreateContactsBatchResults[]> {
249
+ const batchPromises = contactsChunks
250
+ .map((chunkContacts) => async () => this.createContactsInSingleBatch({
251
+ contacts: chunkContacts,
252
+ ...data,
253
+ }));
254
+
255
+ const promisesBatchSize = 1;
256
+ const results: CreateContactsBatchResults[] = [];
257
+
258
+ for (let i = 0;i < batchPromises.length;i += promisesBatchSize) {
259
+ const s = performance.now();
260
+ try {
261
+ const chunk = batchPromises.slice(i, i + promisesBatchSize);
262
+ const chunkResults = await Promise.all(chunk.map((p) => p()));
263
+ results.push(...chunkResults);
264
+ } catch (e) {
265
+ if (e instanceof CreateContactsBatchError) {
266
+ e.processedBatchIds = results.map(({ batchId }) => batchId);
267
+ }
268
+ throw e;
269
+ }
270
+
271
+ const e = performance.now();
272
+ // eslint-disable-next-line no-console
273
+ console.log('batch id = ', i, '; awaiting ', promisesBatchSize, ' batches: ', (e - s) / 1000, 's');
284
274
  }
285
- return batchProcess;
275
+
276
+ return results;
286
277
  }
287
278
 
288
- private async createContactsInSingleBatch(data: CreateMultipleContactsDto): Promise<ContactResponseDto[]> {
279
+ private async createContactsInSingleBatch(data: CreateMultipleContactsDto): Promise<CreateContactsBatchResults> {
280
+ const s = performance.now();
289
281
  const batchProcess = await this.apiCall<BatchProcessResponseDto>({
290
282
  method: 'POST',
291
283
  route: `${this.apiBasePath}/bulk`,
292
284
  data,
293
285
  });
294
286
  const batchProcessResult = await this.polling(batchProcess.id);
295
- const contactIds = batchProcessResult.results.flatMap<string>(i => JSON.parse(i));
296
- const importedContacts = await this.getSafelyContactsList(contactIds, data.contact_book);
297
- return importedContacts.items;
298
- }
299
-
300
- private async createContactsInMultiBatches(
301
- contactsChunks: ContactRequestDto[][],
302
- data: Omit<CreateMultipleContactsDto, 'contacts'>
303
- ): Promise<ContactResponseDto[]> {
304
- const batchPromises = contactsChunks.map((chunkContacts) => async () => this.createContactsInSingleBatch({
305
- contacts: chunkContacts,
306
- ...data,
307
- }));
308
-
309
- const results: ContactResponseDto[] = [];
310
- for (const fn of batchPromises) {
311
- const singleBatchResults = await fn();
312
- results.push(...singleBatchResults);
313
- }
314
- return results;
287
+ const e = performance.now();
288
+ // eslint-disable-next-line no-console
289
+ console.log('single batch time: ', (e - s) / 1000, 's');
290
+ return {
291
+ batchId: batchProcess.id,
292
+ contactsIds: batchProcessResult.results.flatMap<string>(i => JSON.parse(i)),
293
+ };
315
294
  }
316
295
 
317
296
  /**
318
297
  * Pols specific batch process until status of it isn't turned from 'pending to something else.
319
298
  */
320
- private async polling(batchId: string, repeats?: number): Promise<BatchProcessResponseDto> {
321
- const batchProcessState = await this.batchProcessApi.getBatchProcess(batchId).catch((e) => {
322
- repeats = repeats || 0;
299
+ private async polling(batchId: string, repeats = 0): Promise<BatchProcessResponseDto> {
300
+
301
+ let batchProcess: BatchProcessResponseDto;
302
+ try {
303
+ batchProcess = await debouncePromise(() => this.batchProcessApi.getBatchProcess(batchId), 1000);
304
+ // eslint-disable-next-line no-console
305
+ const counter = batchCounters[batchId] || 0;
306
+ // eslint-disable-next-line no-console
307
+ console.log('polling counter of ', batchId, ' = ', counter, ' | status = ', batchProcess.status);
308
+ batchCounters[batchId] = counter + 1;
309
+
310
+ } catch (e) {
323
311
  if (repeats < FAILED_REQUEST_REPEATS) {
324
312
  return this.polling(batchId, repeats + 1);
325
313
  }
326
- throw new Error(e);
327
- });
328
- // value 'pending' is used because of https://onereach.atlassian.net/browse/CU-562
329
- return batchProcessState.status === 'pending' ? this.polling(batchId) : batchProcessState;
314
+ throw new CreateContactsBatchError((e as unknown as Error).message, batchId);
315
+ }
316
+
317
+ if (batchProcess.status === BatchProcessStatus.failed) {
318
+ throw new CreateContactsBatchError(
319
+ 'Could not complete batch process',
320
+ batchId,
321
+ batchProcess.messages
322
+ );
323
+ }
324
+
325
+ return batchProcess.status === BatchProcessStatus.pending
326
+ ? this.polling(batchId)
327
+ : batchProcess;
330
328
  }
331
329
  }
330
+
331
+ const batchCounters: Record<string, number> = {};
package/src/apiError.ts CHANGED
@@ -11,3 +11,14 @@ export class ApiError extends Error {
11
11
  super(message);
12
12
  }
13
13
  }
14
+
15
+ export class CreateContactsBatchError extends Error {
16
+ constructor(
17
+ message: string,
18
+ public readonly failedBatchId: string,
19
+ public readonly batchMessages?: string[],
20
+ public processedBatchIds?: string[],
21
+ ) {
22
+ super(message);
23
+ }
24
+ }
package/src/constants.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export const CONTACTS_SERVICE_KEY = 'contacts-api';
2
2
 
3
- export const REQUEST_PAYLOAD_MAX_BYTES = 128000;
3
+ export const REQUEST_PAYLOAD_MAX_BYTES = 150000;
4
4
 
5
5
  export const FAILED_REQUEST_REPEATS = 3;
6
6
 
7
- export const CONTACTS_DELETE_MAX_AMOUNT = 50;
7
+ /**
8
+ * 5 selected experimentally
9
+ * Since contacts deleting assumes cascading on related field values and deleting
10
+ * relations in contact_book_contact table, it results in huge amount of queries
11
+ * which sent simultaneously might kill connection to the DB.
12
+ */
13
+ export const CONTACTS_DELETE_MAX_AMOUNT = 5;
package/src/types.ts CHANGED
@@ -1,17 +1,8 @@
1
1
  import { Token } from '@or-sdk/base';
2
- import {
3
- ContactBookParamsDto,
4
- ListApiParams,
5
- OrderParams,
6
- DeleteContactMultiParamsDto,
7
- BatchProcessStatus,
8
- } from '@onereach/types-contacts-api';
2
+ import { BatchProcessStatus, ContactBookParamsDto, ListApiParams, OrderParams } from '@onereach/types-contacts-api';
9
3
  import { OrderOptions, PaginationOptions } from '@or-sdk/base';
10
4
  export * from '@onereach/types-contacts-api';
11
5
 
12
- type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
13
- type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
14
-
15
6
  //re-export types
16
7
  export { OrderDirection, PaginationOptions, OrderOptions } from '@or-sdk/base';
17
8
 
@@ -40,7 +31,16 @@ export interface ContactBookParams extends AdaptedListParams<ContactBookParamsDt
40
31
  export type AdaptedListParams<T extends ListApiParams & OrderParams> =
41
32
  Omit<T, 'order' | 'skip' | 'take'> & Partial<PaginationOptions & OrderOptions>;
42
33
 
43
- export type DeleteContactsData = PartialBy<DeleteContactMultiParamsDto, 'ids'> & { all?: boolean;};
34
+ export type CreateContactsBatchResults ={
35
+ batchId: string;
36
+ contactsIds: string[];
37
+ };
38
+
39
+ export type InitCreateBatchResponse = {
40
+ totalChunks: number;
41
+ firstChunkSize: number;
42
+ batchId: string;
43
+ };
44
44
 
45
45
  export type TrackBatchProcessResponse = {
46
46
  status: BatchProcessStatus;
package/src/utils.ts CHANGED
@@ -43,3 +43,19 @@ export const chunkArrByMaxSize = <T>(arr: T[], maxSize: number): T[][] => {
43
43
  ...chunkArrByMaxSize(arr.slice(mid), maxSize),
44
44
  ];
45
45
  };
46
+
47
+ export function debouncePromise<T>(
48
+ caller: () => Promise<T>,
49
+ delay: number,
50
+ reject?: (value: string | T | PromiseLike<T>) => void
51
+ ): Promise<T> {
52
+ let timeout: NodeJS.Timeout | null = null;
53
+
54
+ return new Promise<T>((res) => {
55
+ if (timeout) {
56
+ clearTimeout(timeout);
57
+ }
58
+ timeout = setTimeout(() => caller().then((x) => res(x)).catch((err) => reject && reject(err)), delay);
59
+ });
60
+ }
61
+