@meltwater/conversations-api-services 1.0.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.
@@ -0,0 +1,309 @@
1
+ import { v4 } from 'uuid';
2
+ import superagent from 'superagent';
3
+ import logger from '../../lib/logger.js';
4
+ import configuration from '../../lib/configuration.js';
5
+ import { metricN } from '../../lib/metrics.helper.js';
6
+
7
+ export class IRClient {
8
+ RETRY_COUNT = 50;
9
+ constructor(context) {
10
+ const { company, user, traceId, metrics } = context;
11
+ this.company = company;
12
+ this.user = user;
13
+ this.traceId = traceId;
14
+ this.metrics = metrics;
15
+ }
16
+
17
+ async init() {
18
+ let irApiKey = configuration.get('IR_API_KEY');
19
+
20
+ this.searchServicesUrl = configuration.get('SEARCH_SERVICE_URL');
21
+ this.searchServicesKey = irApiKey;
22
+
23
+ this.documentRevisionsServiceUrl = configuration.get(
24
+ 'DOCUMENT_REVISIONS_SERVICE_URL'
25
+ );
26
+ this.documentServiceUrl = configuration.get('DOCUMENT_SERVICE_URL');
27
+ this.documentServiceKey = irApiKey;
28
+
29
+ this.exportApiUrl = configuration.get('EXPORT_API_URL');
30
+ this.exportApiKey = irApiKey;
31
+ }
32
+
33
+ async addRevisionsToDocuments(operations) {
34
+ await this.init();
35
+ const wait = 100;
36
+ const url = `${this.documentRevisionsServiceUrl}/${this.company._id}/`;
37
+
38
+ let result;
39
+
40
+ try {
41
+ result = await superagent
42
+ .patch(url)
43
+ .query({
44
+ wait,
45
+ apikey: this.documentServiceKey,
46
+ })
47
+ .set('x-client-name', 'engage-conversations')
48
+ .send(operations)
49
+ .then((result) => result.body);
50
+ } catch (error) {
51
+ logger.error(
52
+ `Failed adding a revisions with operation:${JSON.stringify(
53
+ operations
54
+ )}`,
55
+ { error, url, companyId: this.company._id, operations }
56
+ );
57
+ return;
58
+ }
59
+
60
+ logger.info(`Finished adding revision operations`, { operations });
61
+
62
+ return result;
63
+ }
64
+
65
+ async modifyDocuments(operations, retryAttempt = 0) {
66
+ await this.init();
67
+ const wait = 100;
68
+ const url = `${this.documentServiceUrl}/`;
69
+
70
+ let result;
71
+
72
+ try {
73
+ result = await superagent
74
+ .patch(url)
75
+ .query({
76
+ wait,
77
+ apikey: this.documentServiceKey,
78
+ })
79
+ .set('x-client-name', 'engage-conversations')
80
+ .send(operations)
81
+ .then((result) => result.body);
82
+ } catch (err) {
83
+ retryAttempt++;
84
+ if (retryAttempt > this.RETRY_COUNT) {
85
+ logger.error(
86
+ `Maximum Retrys hit for method modifyDocuments ${err.status} ${err.message}`,
87
+ { err, companyId: this.company._id }
88
+ );
89
+ } else if (
90
+ await this.retryBecauseRateLimiting(err, 'modifyDocuments')
91
+ ) {
92
+ return await this.modifyDocuments(operations, retryAttempt);
93
+ }
94
+ }
95
+
96
+ logger.info(`Finished modifying documents operations`, { operations });
97
+
98
+ return result;
99
+ }
100
+
101
+ async getDocumentById(id, retryAttempt = 0) {
102
+ await this.init();
103
+
104
+ try {
105
+ const uri = `${this.documentServiceUrl}/${id}/revisions/${this.company._id}?apikey=${this.documentServiceKey}`;
106
+ const metric = this.metrics.count(metricNames.irGetByDocumentId);
107
+ const result = await superagent.get(uri);
108
+ this.metrics.finishOperation(metricNames.irGetByDocumentId, metric);
109
+ return result.body;
110
+ } catch (err) {
111
+ retryAttempt++;
112
+ if (retryAttempt > this.RETRY_COUNT) {
113
+ logger.error(
114
+ `Maximum Retrys hit for method getDocumentById ${err.status} ${err.message}`,
115
+ { err, companyId: this.company._id }
116
+ );
117
+ } else if (
118
+ await this.retryBecauseRateLimiting(err, 'getDocumentById')
119
+ ) {
120
+ return await this.getDocumentById(id, retryAttempt);
121
+ }
122
+ }
123
+ }
124
+
125
+ async retryBecauseRateLimiting(err, name) {
126
+ // rate limit error
127
+ if (err.status === 429) {
128
+ this.metrics.count(metricNames.irRatelimited);
129
+ logger.error(
130
+ `Error Accessing ir.client method because rate limit: trying again ${name}`,
131
+ { err, companyId: this.company._id }
132
+ );
133
+ // should not be hitting rate limit in production
134
+ // this is a patch for staging testing
135
+ await new Promise((resolve) => setTimeout(resolve, 100));
136
+ return true;
137
+ }
138
+ if (err.status == 404) {
139
+ logger.info(
140
+ `Document Not Found ir.client method ${name} ${err.status} ${err.message}`,
141
+ { err, companyId: this.company._id }
142
+ );
143
+ } else {
144
+ logger.info(
145
+ `Could not access ir.client method ${name} ${err.status} ${err.message}`,
146
+ { err, companyId: this.company._id }
147
+ );
148
+ }
149
+ return false;
150
+ }
151
+
152
+ async export(rune) {
153
+ await this.init();
154
+ const traceId = this.generateTraceId();
155
+
156
+ try {
157
+ let uri = `${this.exportApiUrl}?apikey=${this.exportApiKey}`;
158
+
159
+ logger.info(`Starting call to : ${this.exportApiUrl}`, {
160
+ rune,
161
+ });
162
+
163
+ this.addCompanyToRuneForHorace(rune);
164
+
165
+ let result = await superagent
166
+ .post(uri)
167
+ .query({
168
+ traceId,
169
+ })
170
+ .send(rune);
171
+
172
+ logger.info(
173
+ `Finished calling Export API: ${result.body.exportKey}`,
174
+ {
175
+ irTraceId: traceId,
176
+ }
177
+ );
178
+
179
+ return result.body.exportKey;
180
+ } catch (err) {
181
+ logger.error('Error executing search against Export API', {
182
+ err,
183
+ });
184
+ }
185
+ }
186
+
187
+ async search(rune) {
188
+ await this.init();
189
+ const traceId = this.generateTraceId();
190
+
191
+ try {
192
+ let uri = `${this.searchServicesUrl}?apikey=${this.searchServicesKey}`;
193
+
194
+ // todo: remove changing rune searchResults size
195
+ // todo: remove reduce ( dedupes and trims to original rune limit )
196
+ // rune limit is a bandaid, hopefully better solution can be worked out with Horace
197
+ let limitActual = rune.viewRequests.searchResults.size;
198
+ rune.viewRequests.searchResults.size = limitActual * 2;
199
+
200
+ this.addCompanyToRuneForHorace(rune);
201
+
202
+ const metric = this.metrics.count(metricNames.irSearch);
203
+ let result = await superagent
204
+ .post(uri)
205
+ .query({
206
+ traceId,
207
+ })
208
+ .send(rune);
209
+ this.metrics.finishOperation(metricNames.irSearch, metric);
210
+
211
+ rune.viewRequests.searchResults.size = limitActual;
212
+
213
+ const finalResults = result.body.views.searchResults.results.reduce(
214
+ (acc, document, index) => {
215
+ if (
216
+ !index ||
217
+ (acc.length <= limitActual &&
218
+ acc[acc.length - 1].documentId !==
219
+ document.quiddity.id)
220
+ ) {
221
+ acc.push(document.quiddity);
222
+ } else if (
223
+ index &&
224
+ acc.length <= limitActual &&
225
+ acc[acc.length - 1].documentId === document.quiddity.id
226
+ ) {
227
+ logger.info(
228
+ `Duplicates in IR Search Results for DocumentId ` +
229
+ document.quiddity.id,
230
+ { document, irTraceId: traceId }
231
+ );
232
+ }
233
+ return acc;
234
+ },
235
+ []
236
+ );
237
+
238
+ logger.info(
239
+ `Final Results Calculated, ${
240
+ finalResults ? finalResults.length : 'no results'
241
+ }`,
242
+ { irTraceId: traceId, rune }
243
+ );
244
+
245
+ return finalResults;
246
+ // original return function
247
+ // return result.body.views.searchResults.results.map(document => {
248
+ // return document.quiddity;
249
+ // })
250
+ } catch (err) {
251
+ logger.error('Error executing search against Search Service', {
252
+ err,
253
+ irTraceId: traceId,
254
+ rune,
255
+ });
256
+ }
257
+ }
258
+
259
+ async latest(rune, retryAttempt = 0) {
260
+ try {
261
+ const result = await this.executeSearch(rune);
262
+ return (
263
+ (result.body.views.searchResults.results.length && [
264
+ result.body.views.searchResults.results[0].quiddity,
265
+ ]) ||
266
+ []
267
+ );
268
+ } catch (err) {
269
+ retryAttempt++;
270
+ if (retryAttempt > this.RETRY_COUNT) {
271
+ logger.error(
272
+ `Maximum Retrys hit for method latest ${err.status} ${err.message}`,
273
+ { err, companyId: this.company._id }
274
+ );
275
+ } else if (await this.retryBecauseRateLimiting(err, 'latest')) {
276
+ return await this.latest(rune, retryAttempt);
277
+ }
278
+ }
279
+ }
280
+
281
+ async count(rune) {
282
+ try {
283
+ const result = await this.executeSearch(rune);
284
+ return result.body.views.documentCount.totalCount;
285
+ } catch (err) {
286
+ logger.error('Error getting count from Search Service', err);
287
+ }
288
+ }
289
+
290
+ async executeSearch(rune) {
291
+ await this.init();
292
+ this.addCompanyToRuneForHorace(rune);
293
+ const uri = `${this.searchServicesUrl}?apikey=${this.searchServicesKey}`;
294
+ return await superagent.post(uri).send(rune);
295
+ }
296
+
297
+ addCompanyToRuneForHorace(rune) {
298
+ if (!rune.metaData) {
299
+ rune.metaData = {};
300
+ }
301
+
302
+ // Horace requires this for tracking purposes
303
+ rune.metaData.companyId = this.company._id;
304
+ }
305
+
306
+ generateTraceId() {
307
+ return `engage_${this.company._id}_${this.traceId}_${v4()}`;
308
+ }
309
+ }