@terascope/elasticsearch-api 3.0.0 → 3.1.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.
package/index.js CHANGED
@@ -20,8 +20,12 @@ const {
20
20
  cloneDeep,
21
21
  DataEntity,
22
22
  isDeepEqual,
23
- getTypeOf
23
+ getTypeOf,
24
+ isProd
24
25
  } = require('@terascope/utils');
26
+ const { ElasticsearchDistribution } = require('@terascope/types');
27
+
28
+ const { inspect } = require('util');
25
29
 
26
30
  const DOCUMENT_EXISTS = 409;
27
31
 
@@ -71,8 +75,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
71
75
  }
72
76
 
73
77
  function search(query) {
74
- const esVersion = getESVersion();
75
- if (esVersion >= 7) {
78
+ if (!isElasticsearch6()) {
76
79
  if (query._sourceExclude) {
77
80
  query._sourceExcludes = query._sourceExclude.slice();
78
81
  delete query._sourceExclude;
@@ -226,31 +229,29 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
226
229
  );
227
230
  return Promise.resolve(true);
228
231
  }
229
- return client.cluster.stats({}).then((data) => {
230
- if (!_checkVersion(data.nodes.versions[0])) {
231
- return Promise.resolve();
232
- }
233
- return client.indices
234
- .getSettings({})
235
- .then((results) => {
236
- const resultIndex = _verifyIndex(results, config.index);
237
- if (resultIndex.found) {
238
- resultIndex.indexWindowSize.forEach((ind) => {
239
- logger.warn(
240
- `max_result_window for index: ${ind.name} is set at ${ind.windowSize}. On very large indices it is possible that a slice can not be divided to stay below this limit. If that occurs an error will be thrown by Elasticsearch and the slice can not be processed. Increasing max_result_window in the Elasticsearch index settings will resolve the problem.`
241
- );
242
- });
243
- } else {
244
- const error = new TSError('index specified in reader does not exist', {
245
- statusCode: 404,
246
- });
247
- return Promise.reject(error);
248
- }
249
232
 
250
- return Promise.resolve();
251
- })
252
- .catch((err) => Promise.reject(new TSError(err)));
253
- });
233
+ return client.indices
234
+ .getSettings({})
235
+ .then((results) => {
236
+ const settingsData = results.body && results.meta ? results.body : results;
237
+ const resultIndex = _verifyIndex(settingsData, config.index);
238
+
239
+ if (resultIndex.found) {
240
+ resultIndex.indexWindowSize.forEach((ind) => {
241
+ logger.warn(
242
+ `max_result_window for index: ${ind.name} is set at ${ind.windowSize}. On very large indices it is possible that a slice can not be divided to stay below this limit. If that occurs an error will be thrown by Elasticsearch and the slice can not be processed. Increasing max_result_window in the Elasticsearch index settings will resolve the problem.`
243
+ );
244
+ });
245
+ } else {
246
+ const error = new TSError('index specified in reader does not exist', {
247
+ statusCode: 404,
248
+ });
249
+ return Promise.reject(error);
250
+ }
251
+
252
+ return Promise.resolve();
253
+ })
254
+ .catch((err) => Promise.reject(new TSError(err)));
254
255
  }
255
256
 
256
257
  function putTemplate(template, name) {
@@ -264,7 +265,12 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
264
265
  * When the bulk request has errors this will find the actions
265
266
  * records to retry.
266
267
  *
267
- * @returns {{ retry: Record<string, any>[], error: boolean, reason?: string }}
268
+ * @returns {{
269
+ * retry: Record<string, any>[],
270
+ * successful: number,
271
+ * error: boolean,
272
+ * reason?: string
273
+ * }}
268
274
  */
269
275
  function _filterRetryRecords(actionRecords, result) {
270
276
  const retry = [];
@@ -272,6 +278,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
272
278
 
273
279
  let nonRetriableError = false;
274
280
  let reason = '';
281
+ let successful = 0;
275
282
 
276
283
  for (let i = 0; i < items.length; i++) {
277
284
  // key could either be create or delete etc, just want the actual data at the value spot
@@ -300,14 +307,18 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
300
307
  reason = `${item.error.type}--${item.error.reason}`;
301
308
  break;
302
309
  }
310
+ } else if (item.status == null || item.status < 400) {
311
+ successful++;
303
312
  }
304
313
  }
305
314
 
306
315
  if (nonRetriableError) {
307
- return { retry: [], error: true, reason };
316
+ return {
317
+ retry: [], successful, error: true, reason
318
+ };
308
319
  }
309
320
 
310
- return { retry, error: false };
321
+ return { retry, successful, error: false };
311
322
  }
312
323
 
313
324
  function getFirstKey(obj) {
@@ -319,49 +330,60 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
319
330
  * @returns {Promise<number>}
320
331
  */
321
332
  async function _bulkSend(actionRecords, previousCount = 0, previousRetryDelay = 0) {
322
- const body = actionRecords.flatMap(({ action, data }) => {
323
- if (action == null) {
324
- throw new Error('Bulk send record is missing the action property');
333
+ const body = actionRecords.flatMap((record, index) => {
334
+ if (record.action == null) {
335
+ let dbg = '';
336
+ if (!isProd) {
337
+ dbg = `, dbg: ${inspect({ record, index })}`;
338
+ }
339
+ throw new Error(`Bulk send record is missing the action property${dbg}`);
325
340
  }
326
- if (getESVersion() >= 7) {
327
- const actionKey = getFirstKey(action);
328
- const { _type, ...withoutTypeAction } = action[actionKey];
341
+
342
+ if (!isElasticsearch6()) {
343
+ const actionKey = getFirstKey(record.action);
344
+ const { _type, ...withoutTypeAction } = record.action[actionKey];
329
345
  // if data is specified return both
330
- return data ? [{
331
- ...action,
346
+ return record.data ? [{
347
+ ...record.action,
332
348
  [actionKey]: withoutTypeAction
333
- }, data] : [{
334
- ...action,
349
+ }, record.data] : [{
350
+ ...record.action,
335
351
  [actionKey]: withoutTypeAction
336
352
  }];
337
353
  }
338
354
 
339
355
  // if data is specified return both
340
- return data ? [action, data] : [action];
356
+ return record.data ? [record.action, record.data] : [record.action];
341
357
  });
342
358
 
343
- const result = await _clientRequest('bulk', { body });
344
- if (result.errors) {
345
- const { retry, error, reason } = _filterRetryRecords(actionRecords, result);
359
+ const response = await _clientRequest('bulk', { body });
360
+ const results = response.body ? response.body : response;
346
361
 
347
- if (error) {
348
- throw new TSError(reason, {
349
- retryable: false
350
- });
351
- }
362
+ if (!results.errors) {
363
+ return results.items.reduce((c, item) => {
364
+ const [value] = Object.values(item);
365
+ // ignore non-successful status codes
366
+ if (value.status != null && value.status >= 400) return c;
367
+ return c + 1;
368
+ }, 0);
369
+ }
352
370
 
353
- if (retry.length === 0) {
354
- return previousCount;
355
- }
371
+ const {
372
+ retry, successful, error, reason
373
+ } = _filterRetryRecords(actionRecords, results);
356
374
 
357
- warning();
375
+ if (error) {
376
+ throw new Error(`bulk send error: ${reason}`);
377
+ }
358
378
 
359
- const nextRetryDelay = await _awaitRetry(previousRetryDelay);
360
- const diff = actionRecords.length - retry.length;
361
- return _bulkSend(retry, previousCount + diff, nextRetryDelay);
379
+ if (retry.length === 0) {
380
+ return previousCount + successful;
362
381
  }
363
382
 
364
- return actionRecords.length;
383
+ warning();
384
+
385
+ const nextRetryDelay = await _awaitRetry(previousRetryDelay);
386
+ return _bulkSend(retry, previousCount + successful, nextRetryDelay);
365
387
  }
366
388
 
367
389
  /**
@@ -609,14 +631,19 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
609
631
  client
610
632
  .search(queryParam)
611
633
  .then((data) => {
612
- const { failures, failed } = data._shards;
634
+ const failuresReasons = [];
635
+ const results = data.body ? data.body : data;
636
+ const { failures, failed } = results._shards;
637
+
613
638
  if (!failed) {
614
- resolve(data);
639
+ resolve(results);
615
640
  return;
616
641
  }
617
642
 
643
+ failuresReasons.push(...failures);
644
+
618
645
  const reasons = uniq(
619
- flatten(failures.map((shard) => shard.reason.type))
646
+ flatten(failuresReasons.map((shard) => shard.reason.type))
620
647
  );
621
648
 
622
649
  if (
@@ -643,13 +670,13 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
643
670
  }
644
671
 
645
672
  function _esV7adjustments(queryParam) {
646
- if (getESVersion() >= 7) {
673
+ if (!isElasticsearch6()) {
647
674
  queryParam.trackTotalHits = true;
648
675
  }
649
676
  }
650
677
 
651
678
  function _adjustTypeForEs7(query) {
652
- if (getESVersion() >= 7) {
679
+ if (!isElasticsearch6()) {
653
680
  if (Array.isArray(query)) {
654
681
  return _removeTypeFromBulkRequest(query);
655
682
  }
@@ -660,7 +687,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
660
687
  }
661
688
 
662
689
  function _removeTypeFromBulkRequest(query) {
663
- if (getESVersion() < 7) return query;
690
+ if (isElasticsearch6()) return query;
664
691
 
665
692
  return query.map((queryItem) => {
666
693
  if (isSimpleObject(queryItem)) {
@@ -790,11 +817,6 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
790
817
  };
791
818
  }
792
819
 
793
- function _checkVersion(str) {
794
- const num = Number(str.replace(/\./g, ''));
795
- return num >= 210;
796
- }
797
-
798
820
  function isAvailable(index, recordType) {
799
821
  const query = {
800
822
  index,
@@ -848,7 +870,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
848
870
  });
849
871
  });
850
872
  }
851
-
873
+ // TODO: verifyClient needs to be checked with new client usage
852
874
  /**
853
875
  * Verify the state of the underlying es client.
854
876
  * Will throw error if in fatal state, like the connection is closed.
@@ -872,16 +894,39 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
872
894
 
873
895
  return true;
874
896
  }
875
-
897
+ /** This is deprecated as an external api,
898
+ * please use getClientMetadata
899
+ * */
876
900
  function getESVersion() {
877
- const esVersion = get(client, 'transport._config.apiVersion', '6.5');
901
+ const newClientVersion = get(client, '__meta.version');
902
+ const esVersion = newClientVersion || get(client, 'transport._config.apiVersion', '6.5');
903
+
878
904
  if (esVersion && isString(esVersion)) {
879
905
  const [majorVersion] = esVersion.split('.');
880
906
  return toNumber(majorVersion);
881
907
  }
908
+
882
909
  return 6;
883
910
  }
884
911
 
912
+ function getClientMetadata() {
913
+ const newClientVersion = get(client, '__meta.version');
914
+ const esVersion = newClientVersion || get(client, 'transport._config.apiVersion', '6.5');
915
+ const distribution = get(client, '__meta.distribution', ElasticsearchDistribution.elasticsearch);
916
+
917
+ return {
918
+ distribution,
919
+ version: esVersion
920
+ };
921
+ }
922
+
923
+ function isElasticsearch6() {
924
+ const { distribution, version: esVersion } = getClientMetadata();
925
+ const parsedVersion = toNumber(esVersion.split('.', 1)[0]);
926
+
927
+ return distribution === ElasticsearchDistribution.elasticsearch && parsedVersion === 6;
928
+ }
929
+
885
930
  function _fixMappingRequest(_params, isTemplate) {
886
931
  if (!_params || !_params.body) {
887
932
  throw new Error('Invalid mapping request');
@@ -889,17 +934,14 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
889
934
  const params = cloneDeep(_params);
890
935
  const defaultParams = {};
891
936
 
892
- const esVersion = getESVersion();
893
- if (esVersion >= 6) {
894
- if (params.body.template) {
895
- if (isTemplate) {
896
- params.body.index_patterns = castArray(params.body.template).slice();
897
- }
898
- delete params.body.template;
937
+ if (params.body.template != null) {
938
+ if (isTemplate && params.body.index_patterns == null) {
939
+ params.body.index_patterns = castArray(params.body.template).slice();
899
940
  }
941
+ delete params.body.template;
900
942
  }
901
943
 
902
- if (esVersion >= 7) {
944
+ if (!isElasticsearch6()) {
903
945
  const typeMappings = get(params.body, 'mappings', {});
904
946
  if (typeMappings.properties) {
905
947
  defaultParams.includeTypeName = false;
@@ -913,6 +955,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
913
955
  });
914
956
  }
915
957
  }
958
+
916
959
  return Object.assign({}, defaultParams, params);
917
960
  }
918
961
 
@@ -1011,8 +1054,8 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
1011
1054
 
1012
1055
  function _verifyMapping(query, configMapping, recordType) {
1013
1056
  const params = Object.assign({}, query);
1014
- const esVersion = getESVersion();
1015
- if (esVersion > 6) {
1057
+
1058
+ if (!isElasticsearch6()) {
1016
1059
  if (recordType) {
1017
1060
  params.includeTypeName = true;
1018
1061
  }
@@ -1180,12 +1223,14 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
1180
1223
  indexRecovery,
1181
1224
  indexSetup,
1182
1225
  verifyClient,
1183
- getESVersion,
1184
1226
  validateGeoParameters,
1227
+ getClientMetadata,
1228
+ isElasticsearch6,
1185
1229
  // The APIs below are deprecated and should be removed.
1186
1230
  index_exists: indexExists,
1187
1231
  index_create: indexCreate,
1188
1232
  index_refresh: indexRefresh,
1189
1233
  index_recovery: indexRecovery,
1234
+ getESVersion,
1190
1235
  };
1191
1236
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@terascope/elasticsearch-api",
3
3
  "displayName": "Elasticsearch API",
4
- "version": "3.0.0",
4
+ "version": "3.1.0",
5
5
  "description": "Elasticsearch client api used across multiple services, handles retries and exponential backoff",
6
6
  "homepage": "https://github.com/terascope/teraslice/tree/master/packages/elasticsearch-api#readme",
7
7
  "bugs": {
@@ -18,7 +18,7 @@
18
18
  "test:watch": "ts-scripts test --watch . --"
19
19
  },
20
20
  "dependencies": {
21
- "@terascope/utils": "^0.43.3",
21
+ "@terascope/utils": "^0.44.1",
22
22
  "bluebird": "^3.7.2"
23
23
  },
24
24
  "devDependencies": {
package/test/api-spec.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const Promise = require('bluebird');
4
- const { debugLogger, cloneDeep, DataEntity } = require('@terascope/utils');
4
+ const {
5
+ debugLogger, cloneDeep, DataEntity, isEmpty
6
+ } = require('@terascope/utils');
5
7
  const esApi = require('..');
6
8
 
7
9
  describe('elasticsearch-api', () => {
@@ -189,19 +191,63 @@ describe('elasticsearch-api', () => {
189
191
 
190
192
  function createBulkResponse(results) {
191
193
  const response = { took: 22, errors: false, items: results };
192
- if (bulkError) {
194
+ if (!isEmpty(bulkError)) {
193
195
  response.errors = true;
194
- response.items = results.body.flatMap((obj, index) => {
196
+ let i = -1;
197
+ response.items = results.body.flatMap((obj) => {
195
198
  if (!obj.index && !obj.update && !obj.create && !obj.delete) {
196
199
  // ignore the non-metadata objects
197
200
  return [];
198
201
  }
199
- Object.values(obj).forEach((value) => {
200
- Object.assign(value, {
201
- error: { type: bulkError[index] || 'someType', reason: 'someReason' }
202
- });
203
- });
204
- return [obj];
202
+ i++;
203
+ const [key, value] = Object.entries(obj)[0];
204
+ return [{
205
+ [key]: {
206
+ _index: value._index,
207
+ _type: value._type,
208
+ _id: String(i),
209
+ _version: 1,
210
+ result: `${key}d`,
211
+ error: { type: bulkError[i] || 'someType', reason: 'someReason' },
212
+ _shards: {
213
+ total: 2,
214
+ successful: 1,
215
+ failed: 0
216
+ },
217
+ status: 400,
218
+ _seq_no: 2,
219
+ _primary_term: 3
220
+ }
221
+ }];
222
+ });
223
+ } else {
224
+ response.errors = false;
225
+ let i = -1;
226
+ response.items = results.body.flatMap((obj) => {
227
+ if (!obj.index && !obj.update && !obj.create && !obj.delete) {
228
+ // ignore the non-metadata objects
229
+ return [];
230
+ }
231
+
232
+ i++;
233
+ const [key, value] = Object.entries(obj)[0];
234
+ return [{
235
+ [key]: {
236
+ _index: value._index,
237
+ _type: value._type,
238
+ _id: String(i),
239
+ _version: 1,
240
+ result: `${key}d`,
241
+ _shards: {
242
+ total: 2,
243
+ successful: 1,
244
+ failed: 0
245
+ },
246
+ status: 200,
247
+ _seq_no: 2,
248
+ _primary_term: 3
249
+ }
250
+ }];
205
251
  });
206
252
  }
207
253
  return response;
@@ -314,7 +360,7 @@ describe('elasticsearch-api', () => {
314
360
  stats: () => Promise.resolve({ _nodes: {} })
315
361
  },
316
362
  cluster: {
317
- stats: () => Promise.resolve({ nodes: { versions: ['5.4.1'] } })
363
+ stats: () => Promise.resolve({ nodes: { versions: ['6.8.1'] } })
318
364
  },
319
365
  __testing: {
320
366
  start: 100,
@@ -824,27 +870,20 @@ describe('elasticsearch-api', () => {
824
870
  bulkError = [
825
871
  'es_rejected_execution_exception',
826
872
  'es_rejected_execution_exception',
827
- 'es_rejected_execution_exception'
828
873
  ];
829
874
 
830
- const [results] = await Promise.all([
831
- api.bulkSend(myBulkData),
832
- waitFor(20, () => {
833
- bulkError = false;
834
- })
835
- ]);
875
+ waitFor(20, () => {
876
+ bulkError = false;
877
+ });
878
+ const result = await api.bulkSend(myBulkData);
836
879
 
837
- expect(results).toBeTruthy();
838
- bulkError = ['some_thing_else', 'some_thing_else', 'some_thing_else'];
880
+ expect(result).toBe(2);
839
881
 
840
- return expect(
841
- Promise.all([
842
- api.bulkSend(myBulkData),
843
- waitFor(20, () => {
844
- bulkError = false;
845
- })
846
- ])
847
- ).rejects.toThrow(/some_thing_else--someReason/);
882
+ bulkError = ['some_thing_else', 'some_thing_else'];
883
+
884
+ await expect(
885
+ api.bulkSend(myBulkData)
886
+ ).rejects.toThrow('bulk send error: some_thing_else--someReason');
848
887
  });
849
888
 
850
889
  it('can call buildQuery for geo queries', () => {
package/types/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import * as es from 'elasticsearch';
5
5
  import { Logger } from '@terascope/utils';
6
+ import { ClientMetadata } from '@terascope/types'
6
7
 
7
8
  export = elasticsearchAPI;
8
9
 
@@ -44,7 +45,10 @@ declare namespace elasticsearchAPI {
44
45
  indexSetup: (clusterName, newIndex, migrantIndexName, mapping, recordType, clientName) => Promise<boolean>;
45
46
  verifyClient: () => boolean;
46
47
  validateGeoParameters: (opConfig: any) => any;
48
+ /** This api is deprecated, please use getClientMetadata */
47
49
  getESVersion: () => number;
50
+ getClientMetadata: () => ClientMetadata;
51
+ isElasticsearch6: () => boolean
48
52
  }
49
53
 
50
54
  /**