@or-sdk/contacts 3.2.2-beta.1774.0 → 3.2.2-beta.1778.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 -166
  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 -93
  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 -4
  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 -130
  43. package/src/apiError.ts +11 -0
  44. package/src/constants.ts +8 -2
  45. package/src/types.ts +11 -10
  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,47 +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, notifyEventProxy = false): 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
- notifyEventProxy,
114
- });
115
- }
116
-
117
- /**
118
- * Initiates bulk of deleting all contacts from a certain book.
119
- * The only first batch is going to be executed and the only - its
120
- * process id is returned in the response.
121
- * Tracking of the batch status is possible with trackBatchProcess
122
- * from BatchProcessApi
123
- *
124
- * @see batchProcessApi.trackBatchProcess
125
- */
126
- async initDeleteBookContactsBulk(bookId: string, notifyEventProxy = false) {
127
- const { contactsCount } = await this.bookServiceApi.getContactBook(bookId);
128
- const contacts = await this.listContact({
129
- contact_book: bookId,
130
- size: CONTACTS_DELETE_MAX_AMOUNT,
131
- });
132
-
133
- const batchProcess = await this.runSingleDeleteContactsBulk({
134
- ids: contacts.items.map(({ id }) => id),
135
- contact_book: bookId,
136
- notifyEventProxy,
137
- });
138
-
139
- return {
140
- batchId: batchProcess.id,
141
- totalBatches: Math.floor(contactsCount / 50),
142
- firstBatchSize: CONTACTS_DELETE_MAX_AMOUNT,
143
- };
144
- }
145
-
146
104
  /**
147
105
  * @description Create Contact
148
106
  * @param data
@@ -157,18 +115,61 @@ export default class ContactApi extends BaseApi {
157
115
 
158
116
  /**
159
117
  * @description Create Contacts either in single or in multi batch(es), depending on payload size
160
- * @param data
161
118
  */
162
- 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);
163
122
  const { contacts, ...rest } = data;
164
123
  const contactsMaxSize = REQUEST_PAYLOAD_MAX_BYTES - getObjectSizeInBytes({ ...rest });
165
124
  const contactsChunks = chunkArrByMaxSize(contacts, contactsMaxSize);
166
125
 
167
- return contactsChunks.length === 1
168
- ? this.createContactsInSingleBatch(data)
169
- : this.createContactsInMultiBatches(contactsChunks, rest);
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]), []);
170
137
  }
171
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> {
152
+ const { contacts, ...rest } = data;
153
+ const contactsMaxSize = REQUEST_PAYLOAD_MAX_BYTES - getObjectSizeInBytes({ ...rest });
154
+ const contactsChunks = chunkArrByMaxSize(contacts, contactsMaxSize);
155
+
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
+ };
170
+ }
171
+
172
+
172
173
  /**
173
174
  * @description Merge two Contacts into one
174
175
  * @param id Contact id TO which the data will be merged
@@ -232,103 +233,99 @@ export default class ContactApi extends BaseApi {
232
233
  });
233
234
  }
234
235
 
235
- private async getSafelyContactsList(
236
- contactIds: string[],
237
- bookId: string | undefined,
238
- repeats?: number
239
- ): Promise<List<ContactResponseDto>> {
240
- const contacts = await this.listContact({
241
- contactIds,
242
- ...(bookId && { contact_book: bookId }),
243
- }).catch((e) => {
244
- repeats = repeats || 0;
245
- if (repeats < FAILED_REQUEST_REPEATS) {
246
- return this.getSafelyContactsList(contactIds, bookId, repeats + 1);
247
- }
248
- throw new Error(e);
249
- });
250
- return contacts;
251
- }
252
-
253
- private async bulkDeleteContacts(data: DeleteContactsData): Promise<void> {
254
- const { ids, contact_book, all, ...rest } = data;
255
- if (all) {
256
- if (!contact_book) {
257
- throw new ApiError(400, 'contact_book should be provided in case if "all" is true');
258
- }
259
- return await this.deleteContactsByBook(contact_book);
260
- }
261
- if (!ids) {
262
- throw new ApiError(400, 'Provide either "contact_book" or "all"');
263
- }
264
- const batchSize = CONTACTS_DELETE_MAX_AMOUNT;
265
- for (let i = 0;i < ids.length;i += batchSize) {
266
- const chunk = ids.slice(i, i + batchSize);
267
- await this.runSingleDeleteContactsBulk({
268
- ids: chunk,
269
- contact_book,
270
- ...rest,
271
- });
272
- }
273
- }
274
-
275
- private async runSingleDeleteContactsBulk(
276
- data: DeleteContactMultiParamsDto,
277
- withPoling = true
278
- ): Promise<BatchProcessResponseDto> {
236
+ private async bulkDeleteContacts(data: DeleteContactMultiParamsDto): Promise<void> {
279
237
  const batchProcess = await this.apiCall<BatchProcessResponseDto>({
280
238
  method: 'DELETE',
281
239
  route: `${this.apiBasePath}/bulk`,
282
240
  data,
283
241
  });
284
- if (withPoling) {
285
- const result = await this.polling(batchProcess.id);
286
- 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 = 2;
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');
287
274
  }
288
- return batchProcess;
275
+
276
+ return results;
289
277
  }
290
278
 
291
- private async createContactsInSingleBatch(data: CreateMultipleContactsDto): Promise<ContactResponseDto[]> {
279
+ private async createContactsInSingleBatch(data: CreateMultipleContactsDto): Promise<CreateContactsBatchResults> {
280
+ const s = performance.now();
292
281
  const batchProcess = await this.apiCall<BatchProcessResponseDto>({
293
282
  method: 'POST',
294
283
  route: `${this.apiBasePath}/bulk`,
295
284
  data,
296
285
  });
297
286
  const batchProcessResult = await this.polling(batchProcess.id);
298
- const contactIds = batchProcessResult.results.flatMap<string>(i => JSON.parse(i));
299
- const importedContacts = await this.getSafelyContactsList(contactIds, data.contact_book);
300
- return importedContacts.items;
301
- }
302
-
303
- private async createContactsInMultiBatches(
304
- contactsChunks: ContactRequestDto[][],
305
- data: Omit<CreateMultipleContactsDto, 'contacts'>
306
- ): Promise<ContactResponseDto[]> {
307
- const batchPromises = contactsChunks.map((chunkContacts) => async () => this.createContactsInSingleBatch({
308
- contacts: chunkContacts,
309
- ...data,
310
- }));
311
-
312
- const results: ContactResponseDto[] = [];
313
- for (const fn of batchPromises) {
314
- const singleBatchResults = await fn();
315
- results.push(...singleBatchResults);
316
- }
317
- 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
+ };
318
294
  }
319
295
 
320
296
  /**
321
297
  * Pols specific batch process until status of it isn't turned from 'pending to something else.
322
298
  */
323
- private async polling(batchId: string, repeats?: number): Promise<BatchProcessResponseDto> {
324
- const batchProcessState = await this.batchProcessApi.getBatchProcess(batchId).catch((e) => {
325
- 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) {
326
311
  if (repeats < FAILED_REQUEST_REPEATS) {
327
312
  return this.polling(batchId, repeats + 1);
328
313
  }
329
- throw new Error(e);
330
- });
331
- // value 'pending' is used because of https://onereach.atlassian.net/browse/CU-562
332
- 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;
333
328
  }
334
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,16 +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 OptionalBy<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
13
-
14
6
  //re-export types
15
7
  export { OrderDirection, PaginationOptions, OrderOptions } from '@or-sdk/base';
16
8
 
@@ -39,7 +31,16 @@ export interface ContactBookParams extends AdaptedListParams<ContactBookParamsDt
39
31
  export type AdaptedListParams<T extends ListApiParams & OrderParams> =
40
32
  Omit<T, 'order' | 'skip' | 'take'> & Partial<PaginationOptions & OrderOptions>;
41
33
 
42
- export type DeleteContactsData = OptionalBy<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
+ };
43
44
 
44
45
  export type TrackBatchProcessResponse = {
45
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
+