kuzzle 2.31.0 → 2.32.0-beta.1

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 (41) hide show
  1. package/index.d.ts +1 -0
  2. package/index.js +1 -0
  3. package/lib/api/controllers/authController.d.ts +3 -2
  4. package/lib/api/funnel.js +2 -1
  5. package/lib/config/default.config.js +1 -0
  6. package/lib/core/backend/backendStorage.d.ts +3 -5
  7. package/lib/core/backend/backendStorage.js +8 -10
  8. package/lib/core/plugin/pluginContext.d.ts +2 -3
  9. package/lib/core/plugin/pluginContext.js +6 -4
  10. package/lib/core/security/tokenRepository.d.ts +1 -1
  11. package/lib/core/security/tokenRepository.js +1 -1
  12. package/lib/core/shared/ObjectRepository.d.ts +1 -1
  13. package/lib/core/storage/clientAdapter.js +6 -4
  14. package/lib/core/storage/storageEngine.js +4 -5
  15. package/lib/kerror/index.js +1 -1
  16. package/lib/kuzzle/event/KuzzleEventEmitter.d.ts +70 -0
  17. package/lib/kuzzle/event/KuzzleEventEmitter.js +328 -0
  18. package/lib/kuzzle/index.d.ts +3 -0
  19. package/lib/kuzzle/index.js +7 -4
  20. package/lib/kuzzle/kuzzle.d.ts +32 -19
  21. package/lib/kuzzle/kuzzle.js +31 -31
  22. package/lib/service/storage/{elasticsearch.d.ts → 7/elasticsearch.d.ts} +40 -22
  23. package/lib/service/storage/{elasticsearch.js → 7/elasticsearch.js} +24 -43
  24. package/lib/service/storage/{esWrapper.js → 7/esWrapper.js} +6 -4
  25. package/lib/service/storage/8/elasticsearch.d.ts +972 -0
  26. package/lib/service/storage/8/elasticsearch.js +2925 -0
  27. package/lib/service/storage/8/esWrapper.js +303 -0
  28. package/lib/service/storage/Elasticsearch.d.ts +9 -0
  29. package/lib/service/storage/Elasticsearch.js +48 -0
  30. package/lib/service/storage/commons/queryTranslator.d.ts +5 -0
  31. package/lib/service/storage/commons/queryTranslator.js +189 -0
  32. package/lib/types/EventHandler.d.ts +29 -1
  33. package/lib/types/config/KuzzleConfiguration.d.ts +2 -1
  34. package/lib/types/config/storageEngine/StorageEngineElasticsearchConfiguration.d.ts +6 -2
  35. package/lib/types/storage/{Elasticsearch.d.ts → 7/Elasticsearch.d.ts} +1 -1
  36. package/lib/types/storage/8/Elasticsearch.d.ts +59 -0
  37. package/lib/types/storage/8/Elasticsearch.js +3 -0
  38. package/package.json +7 -4
  39. package/lib/kuzzle/event/kuzzleEventEmitter.js +0 -405
  40. package/lib/service/storage/queryTranslator.js +0 -219
  41. /package/lib/types/storage/{Elasticsearch.js → 7/Elasticsearch.js} +0 -0
@@ -0,0 +1,2925 @@
1
+ "use strict";
2
+ /*
3
+ * Kuzzle, a backend software, self-hostable and ready to use
4
+ * to power modern apps
5
+ *
6
+ * Copyright 2015-2022 Kuzzle
7
+ * mailto: support AT kuzzle.io
8
+ * website: http://kuzzle.io
9
+ *
10
+ * Licensed under the Apache License, Version 2.0 (the "License");
11
+ * you may not use this file except in compliance with the License.
12
+ * You may obtain a copy of the License at
13
+ *
14
+ * https://www.apache.org/licenses/LICENSE-2.0
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.ES8 = void 0;
50
+ const lodash_1 = __importDefault(require("lodash"));
51
+ const sdk_es8_1 = require("sdk-es8");
52
+ const assert_1 = __importDefault(require("assert"));
53
+ const ms_1 = __importDefault(require("ms"));
54
+ const bluebird_1 = __importDefault(require("bluebird"));
55
+ const semver_1 = __importDefault(require("semver"));
56
+ const debug_1 = __importDefault(require("../../../util/debug"));
57
+ const esWrapper_1 = __importDefault(require("./esWrapper"));
58
+ const queryTranslator_1 = require("../commons/queryTranslator");
59
+ const didYouMean_1 = __importDefault(require("../../../util/didYouMean"));
60
+ const kerror = __importStar(require("../../../kerror"));
61
+ const requestAssertions_1 = require("../../../util/requestAssertions");
62
+ const safeObject_1 = require("../../../util/safeObject");
63
+ const storeScopeEnum_1 = require("../../../core/storage/storeScopeEnum");
64
+ const extractFields_1 = __importDefault(require("../../../util/extractFields"));
65
+ const mutex_1 = require("../../../util/mutex");
66
+ const name_generator_1 = require("../../../util/name-generator");
67
+ (0, debug_1.default)("kuzzle:services:elasticsearch");
68
+ const SCROLL_CACHE_PREFIX = "_docscroll_";
69
+ const ROOT_MAPPING_PROPERTIES = [
70
+ "properties",
71
+ "_meta",
72
+ "dynamic",
73
+ "dynamic_templates",
74
+ ];
75
+ const CHILD_MAPPING_PROPERTIES = ["type"];
76
+ // Used for collection emulation
77
+ const HIDDEN_COLLECTION = "_kuzzle_keep";
78
+ const ALIAS_PREFIX = "@"; // @todo next major release: Add ALIAS_PREFIX in FORBIDDEN_CHARS
79
+ const PRIVATE_PREFIX = "%";
80
+ const PUBLIC_PREFIX = "&";
81
+ const INDEX_PREFIX_POSITION_IN_INDICE = 0;
82
+ const INDEX_PREFIX_POSITION_IN_ALIAS = 1;
83
+ const NAME_SEPARATOR = ".";
84
+ const FORBIDDEN_CHARS = `\\/*?"<>| \t\r\n,+#:${NAME_SEPARATOR}${PUBLIC_PREFIX}${PRIVATE_PREFIX}`;
85
+ const DYNAMIC_PROPERTY_VALUES = ["true", "false", "strict"];
86
+ // used to check whether we need to wait for ES to initialize or not
87
+ var esStateEnum;
88
+ (function (esStateEnum) {
89
+ esStateEnum[esStateEnum["AWAITING"] = 1] = "AWAITING";
90
+ esStateEnum[esStateEnum["NONE"] = 2] = "NONE";
91
+ esStateEnum[esStateEnum["OK"] = 3] = "OK";
92
+ })(esStateEnum || (esStateEnum = {}));
93
+ let esState = esStateEnum.NONE;
94
+ /**
95
+ * @param {Kuzzle} kuzzle kuzzle instance
96
+ * @param {Object} config Service configuration
97
+ * @param {storeScopeEnum} scope
98
+ * @constructor
99
+ */
100
+ class ES8 {
101
+ constructor(config, scope = storeScopeEnum_1.storeScopeEnum.PUBLIC) {
102
+ this._config = config;
103
+ this._scope = scope;
104
+ this._indexPrefix =
105
+ scope === storeScopeEnum_1.storeScopeEnum.PRIVATE ? PRIVATE_PREFIX : PUBLIC_PREFIX;
106
+ this._client = null;
107
+ this._esWrapper = null;
108
+ this._esVersion = null;
109
+ this._translator = new queryTranslator_1.QueryTranslator();
110
+ // Allowed root key of a search query
111
+ this.searchBodyKeys = [
112
+ "aggregations",
113
+ "aggs",
114
+ "collapse",
115
+ "explain",
116
+ "fields",
117
+ "from",
118
+ "highlight",
119
+ "query",
120
+ "search_after",
121
+ "search_timeout",
122
+ "size",
123
+ "sort",
124
+ "suggest",
125
+ "_name",
126
+ "_source",
127
+ "_source_excludes",
128
+ "_source_includes",
129
+ ];
130
+ /**
131
+ * Only allow stored-scripts in queries
132
+ */
133
+ this.scriptKeys = ["script", "_script"];
134
+ this.scriptAllowedArgs = ["id", "params"];
135
+ this.maxScrollDuration = this._loadMsConfig("maxScrollDuration");
136
+ this.scrollTTL = this._loadMsConfig("defaults.scrollTTL");
137
+ }
138
+ get scope() {
139
+ return this._scope;
140
+ }
141
+ /**
142
+ * Initializes the elasticsearch client
143
+ *
144
+ * @override
145
+ * @returns {Promise}
146
+ */
147
+ async _initSequence() {
148
+ if (this._client) {
149
+ return;
150
+ }
151
+ if (global.NODE_ENV !== "development" &&
152
+ this._config.commonMapping.dynamic === "true") {
153
+ global.kuzzle.log.warn([
154
+ "Your dynamic mapping policy is set to 'true' for new fields.",
155
+ "Elasticsearch will try to automatically infer mapping for new fields, and those cannot be changed afterward.",
156
+ 'See the "services.storageEngine.commonMapping.dynamic" option in the kuzzlerc configuration file to change this value.',
157
+ ].join("\n"));
158
+ }
159
+ this._client = new sdk_es8_1.Client(this._config.client);
160
+ await this.waitForElasticsearch();
161
+ this._esWrapper = new esWrapper_1.default(this._client);
162
+ const { version } = await this._client.info();
163
+ if (version &&
164
+ !semver_1.default.satisfies(semver_1.default.coerce(version.number), ">=8.0.0")) {
165
+ throw kerror.get("services", "storage", "version_mismatch", version.number);
166
+ }
167
+ this._esVersion = version;
168
+ }
169
+ /**
170
+ * Translate Koncorde filters to Elasticsearch query
171
+ *
172
+ * @param {Object} filters - Set of valid Koncorde filters
173
+ * @returns {Object} Equivalent Elasticsearch query
174
+ */
175
+ translateKoncordeFilters(filters) {
176
+ return this._translator.translate(filters);
177
+ }
178
+ /**
179
+ * Returns some basic information about this service
180
+ * @override
181
+ *
182
+ * @returns {Promise.<InfoResult>} service informations
183
+ */
184
+ async info() {
185
+ const result = {
186
+ type: "elasticsearch",
187
+ version: this._esVersion,
188
+ };
189
+ try {
190
+ const info = await this._client.info();
191
+ result.version = info.version.number;
192
+ result.lucene = info.version.lucene_version;
193
+ const health = await this._client.cluster.health();
194
+ result.status = health.status;
195
+ const stats = await this._client.cluster.stats({ human: true });
196
+ result.spaceUsed = stats.indices.store.size;
197
+ result.nodes = stats.nodes;
198
+ return result;
199
+ }
200
+ catch (error) {
201
+ return this._esWrapper.reject(error);
202
+ }
203
+ }
204
+ /**
205
+ * Returns detailed multi-level storage stats data
206
+ *
207
+ * @returns {Promise.<Object>}
208
+ */
209
+ async stats() {
210
+ const esRequest = {
211
+ metric: ["docs", "store"],
212
+ };
213
+ const stats = await this._client.indices.stats(esRequest);
214
+ const indexes = {};
215
+ let size = 0;
216
+ for (const [indice, indiceInfo] of Object.entries(stats.indices)) {
217
+ const infos = indiceInfo;
218
+ // Ignore non-Kuzzle indices
219
+ if (!indice.startsWith(PRIVATE_PREFIX) &&
220
+ !indice.startsWith(PUBLIC_PREFIX)) {
221
+ continue;
222
+ }
223
+ const aliases = await this._getAliasFromIndice(indice);
224
+ const alias = aliases[0];
225
+ const indexName = this._extractIndex(alias);
226
+ const collectionName = this._extractCollection(alias);
227
+ if (alias[INDEX_PREFIX_POSITION_IN_ALIAS] !== this._indexPrefix ||
228
+ collectionName === HIDDEN_COLLECTION) {
229
+ continue;
230
+ }
231
+ if (!indexes[indexName]) {
232
+ indexes[indexName] = {
233
+ collections: [],
234
+ name: indexName,
235
+ size: 0,
236
+ };
237
+ }
238
+ indexes[indexName].collections.push({
239
+ documentCount: infos.total.docs.count,
240
+ name: collectionName,
241
+ size: infos.total.store.size_in_bytes,
242
+ });
243
+ indexes[indexName].size += infos.total.store.size_in_bytes;
244
+ size += infos.total.store.size_in_bytes;
245
+ }
246
+ return {
247
+ indexes: Object.values(indexes),
248
+ size,
249
+ };
250
+ }
251
+ /**
252
+ * Scrolls results from previous elasticsearch query.
253
+ * Automatically clears the scroll context after the last result page has
254
+ * been fetched.
255
+ *
256
+ * @param {String} scrollId - Scroll identifier
257
+ * @param {Object} options - scrollTTL (default scrollTTL)
258
+ *
259
+ * @returns {Promise.<{ scrollId, hits, aggregations, total }>}
260
+ */
261
+ async scroll(scrollId, { scrollTTL } = {}) {
262
+ const _scrollTTL = scrollTTL || this._config.defaults.scrollTTL;
263
+ const esRequest = {
264
+ scroll: _scrollTTL,
265
+ scroll_id: scrollId,
266
+ };
267
+ const cacheKey = SCROLL_CACHE_PREFIX + global.kuzzle.hash(esRequest.scroll_id);
268
+ (0, debug_1.default)("Scroll: %o", esRequest);
269
+ if (_scrollTTL) {
270
+ const scrollDuration = (0, ms_1.default)(_scrollTTL);
271
+ if (scrollDuration > this.maxScrollDuration) {
272
+ throw kerror.get("services", "storage", "scroll_duration_too_great", _scrollTTL);
273
+ }
274
+ }
275
+ const stringifiedScrollInfo = await global.kuzzle.ask("core:cache:internal:get", cacheKey);
276
+ if (!stringifiedScrollInfo) {
277
+ throw kerror.get("services", "storage", "unknown_scroll_id");
278
+ }
279
+ const scrollInfo = JSON.parse(stringifiedScrollInfo);
280
+ try {
281
+ const body = await this._client.scroll(esRequest);
282
+ const totalHitsValue = this._getHitsTotalValue(body.hits);
283
+ scrollInfo.fetched += body.hits.hits.length;
284
+ if (scrollInfo.fetched >= totalHitsValue) {
285
+ (0, debug_1.default)("Last scroll page fetched: deleting scroll %s", body._scroll_id);
286
+ await global.kuzzle.ask("core:cache:internal:del", cacheKey);
287
+ await this.clearScroll(body._scroll_id);
288
+ }
289
+ else {
290
+ await global.kuzzle.ask("core:cache:internal:store", cacheKey, JSON.stringify(scrollInfo), {
291
+ ttl: (0, ms_1.default)(_scrollTTL) || this.scrollTTL,
292
+ });
293
+ }
294
+ const remaining = totalHitsValue - scrollInfo.fetched;
295
+ return await this._formatSearchResult(body, remaining, scrollInfo);
296
+ }
297
+ catch (error) {
298
+ throw this._esWrapper.formatESError(error);
299
+ }
300
+ }
301
+ /**
302
+ * Searches documents from elasticsearch with a query
303
+ *
304
+ * @param {String} index - Index name
305
+ * @param {String} collection - Collection name
306
+ * @param {Object} searchBody - Search request body (query, sort, etc.)
307
+ * @param {Object} options - from (undefined), size (undefined), scroll (undefined)
308
+ *
309
+ * @returns {Promise.<{ scrollId, hits, aggregations, suggest, total }>}
310
+ */
311
+ async search({ index, collection, searchBody, targets, } = {}, { from, size, scroll, } = {}) {
312
+ let esIndexes;
313
+ if (targets && targets.length > 0) {
314
+ const indexes = new Set();
315
+ for (const target of targets) {
316
+ for (const targetCollection of target.collections) {
317
+ const alias = this._getAlias(target.index, targetCollection);
318
+ indexes.add(alias);
319
+ }
320
+ }
321
+ esIndexes = Array.from(indexes).join(",");
322
+ }
323
+ else {
324
+ esIndexes = this._getAlias(index, collection);
325
+ }
326
+ const esRequest = {
327
+ ...this._sanitizeSearchBody(searchBody),
328
+ from,
329
+ index: esIndexes,
330
+ scroll,
331
+ size,
332
+ track_total_hits: true,
333
+ };
334
+ if (scroll) {
335
+ const scrollDuration = (0, ms_1.default)(scroll);
336
+ if (scrollDuration > this.maxScrollDuration) {
337
+ throw kerror.get("services", "storage", "scroll_duration_too_great", scroll);
338
+ }
339
+ }
340
+ (0, debug_1.default)("Search: %j", esRequest);
341
+ try {
342
+ const body = await this._client.search(esRequest);
343
+ const totalHitsValue = this._getHitsTotalValue(body.hits);
344
+ let remaining;
345
+ if (body._scroll_id) {
346
+ const ttl = (esRequest.scroll && (0, ms_1.default)(esRequest.scroll)) ||
347
+ (0, ms_1.default)(this._config.defaults.scrollTTL);
348
+ await global.kuzzle.ask("core:cache:internal:store", SCROLL_CACHE_PREFIX + global.kuzzle.hash(body._scroll_id), JSON.stringify({
349
+ collection,
350
+ fetched: body.hits.hits.length,
351
+ index,
352
+ targets,
353
+ }), { ttl });
354
+ remaining = totalHitsValue - body.hits.hits.length;
355
+ }
356
+ return await this._formatSearchResult(body, remaining, {
357
+ collection,
358
+ index,
359
+ targets,
360
+ });
361
+ }
362
+ catch (error) {
363
+ throw this._esWrapper.formatESError(error);
364
+ }
365
+ }
366
+ /**
367
+ * Generate a map that associate an alias to a pair of index and collection
368
+ *
369
+ * @param {*} targets
370
+ * @returns
371
+ */
372
+ _mapTargetsToAlias(targets) {
373
+ const aliasToTargets = {};
374
+ for (const target of targets) {
375
+ for (const targetCollection of target.collections) {
376
+ const alias = this._getAlias(target.index, targetCollection);
377
+ if (!aliasToTargets[alias]) {
378
+ aliasToTargets[alias] = {
379
+ collection: targetCollection,
380
+ index: target.index,
381
+ };
382
+ }
383
+ }
384
+ }
385
+ return aliasToTargets;
386
+ }
387
+ async _formatSearchResult(body, remaining, searchInfo = {}) {
388
+ let aliasToTargets = {};
389
+ const aliasCache = new Map();
390
+ if (searchInfo.targets) {
391
+ /**
392
+ * We need to map the alias to the target index and collection,
393
+ * so we can later retrieve informations about an index & collection
394
+ * based on its alias.
395
+ */
396
+ aliasToTargets = this._mapTargetsToAlias(searchInfo.targets);
397
+ }
398
+ const formatHit = async (hit) => {
399
+ let index = searchInfo.index;
400
+ let collection = searchInfo.collection;
401
+ /**
402
+ * If the search has been done on multiple targets, we need to
403
+ * retrieve the appropriate index and collection based on the alias
404
+ */
405
+ if (hit._index && searchInfo.targets) {
406
+ // Caching to reduce call to ES
407
+ let aliases = aliasCache.get(hit._index);
408
+ if (!aliases) {
409
+ // Retrieve all the alias associated to one index
410
+ aliases = await this._getAliasFromIndice(hit._index);
411
+ aliasCache.set(hit._index, aliases);
412
+ }
413
+ /**
414
+ * Since multiple alias can point to the same index in ES, we need to
415
+ * find the first alias that exists in the map of aliases associated
416
+ * to the targets.
417
+ */
418
+ const alias = aliases.find((_alias) => aliasToTargets[_alias]);
419
+ // Retrieve index and collection information based on the matching alias
420
+ index = aliasToTargets[alias].index;
421
+ collection = aliasToTargets[alias].collection;
422
+ }
423
+ return {
424
+ _id: hit._id,
425
+ _score: hit._score,
426
+ _source: hit._source,
427
+ collection,
428
+ highlight: hit.highlight,
429
+ index,
430
+ };
431
+ };
432
+ async function formatInnerHits(innerHits) {
433
+ if (!innerHits) {
434
+ return undefined;
435
+ }
436
+ const formattedInnerHits = {};
437
+ for (const [name, innerHit] of Object.entries(innerHits)) {
438
+ formattedInnerHits[name] = await bluebird_1.default.map(innerHit.hits.hits, formatHit);
439
+ }
440
+ return formattedInnerHits;
441
+ }
442
+ const hits = await bluebird_1.default.map(body.hits.hits, async (hit) => ({
443
+ inner_hits: await formatInnerHits(hit.inner_hits),
444
+ ...(await formatHit(hit)),
445
+ }));
446
+ return {
447
+ aggregations: body.aggregations,
448
+ hits,
449
+ remaining,
450
+ scrollId: body._scroll_id,
451
+ suggest: body.suggest,
452
+ total: body.hits.total.value,
453
+ };
454
+ }
455
+ /**
456
+ * Gets the document with given ID
457
+ *
458
+ * @param {String} index - Index name
459
+ * @param {String} collection - Collection name
460
+ * @param {String} id - Document ID
461
+ *
462
+ * @returns {Promise.<{ _id, _version, _source }>}
463
+ */
464
+ async get(index, collection, id) {
465
+ const esRequest = {
466
+ id,
467
+ index: this._getAlias(index, collection),
468
+ };
469
+ // Just in case the user make a GET on url /mainindex/test/_search
470
+ // Without this test we return something weird: a result.hits.hits with all
471
+ // document without filter because the body is empty in HTTP by default
472
+ if (esRequest.id === "_search") {
473
+ return kerror.reject("services", "storage", "search_as_an_id");
474
+ }
475
+ (0, debug_1.default)("Get document: %o", esRequest);
476
+ try {
477
+ const body = await this._client.get(esRequest);
478
+ return {
479
+ _id: body._id,
480
+ _source: body._source,
481
+ _version: body._version,
482
+ };
483
+ }
484
+ catch (error) {
485
+ throw this._esWrapper.formatESError(error);
486
+ }
487
+ }
488
+ /**
489
+ * Returns the list of documents matching the ids given in the body param
490
+ * NB: Due to internal Kuzzle mechanism, can only be called on a single
491
+ * index/collection, using the body { ids: [.. } syntax.
492
+ *
493
+ * @param {String} index - Index name
494
+ * @param {String} collection - Collection name
495
+ * @param {Array.<String>} ids - Document IDs
496
+ *
497
+ * @returns {Promise.<{ items: Array<{ _id, _source, _version }>, errors }>}
498
+ */
499
+ async mGet(index, collection, ids) {
500
+ if (ids.length === 0) {
501
+ return { errors: [], item: [] };
502
+ }
503
+ const esRequest = {
504
+ docs: ids.map((_id) => ({
505
+ _id,
506
+ _index: this._getAlias(index, collection),
507
+ })),
508
+ };
509
+ (0, debug_1.default)("Multi-get documents: %o", esRequest);
510
+ let body;
511
+ try {
512
+ body = await this._client.mget(esRequest); // NOSONAR
513
+ }
514
+ catch (e) {
515
+ throw this._esWrapper.formatESError(e);
516
+ }
517
+ const errors = [];
518
+ const items = [];
519
+ for (const doc of body.docs) {
520
+ if (!("error" in doc) && doc.found) {
521
+ items.push({
522
+ _id: doc._id,
523
+ _source: doc._source,
524
+ _version: doc._version,
525
+ });
526
+ }
527
+ else {
528
+ errors.push(doc._id);
529
+ }
530
+ }
531
+ return { errors, items };
532
+ }
533
+ /**
534
+ * Counts how many documents match the filter given in body
535
+ *
536
+ * @param {String} index - Index name
537
+ * @param {String} collection - Collection name
538
+ * @param {Object} searchBody - Search request body (query, sort, etc.)
539
+ *
540
+ * @returns {Promise.<Number>} count
541
+ */
542
+ async count(index, collection, searchBody = {}) {
543
+ const esRequest = {
544
+ ...this._sanitizeSearchBody(searchBody),
545
+ index: this._getAlias(index, collection),
546
+ };
547
+ (0, debug_1.default)("Count: %o", esRequest);
548
+ try {
549
+ const body = await this._client.count(esRequest);
550
+ return body.count;
551
+ }
552
+ catch (error) {
553
+ throw this._esWrapper.formatESError(error);
554
+ }
555
+ }
556
+ /**
557
+ * Sends the new document to elasticsearch
558
+ * Cleans data to match elasticsearch specifications
559
+ *
560
+ * @param {String} index - Index name
561
+ * @param {String} collection - Collection name
562
+ * @param {Object} content - Document content
563
+ * @param {Object} options - id (undefined), refresh (undefined), userId (null)
564
+ *
565
+ * @returns {Promise.<Object>} { _id, _version, _source }
566
+ */
567
+ async create(index, collection, content, { id, refresh, userId = null, injectKuzzleMeta = true, } = {}) {
568
+ (0, requestAssertions_1.assertIsObject)(content);
569
+ const esRequest = {
570
+ document: content,
571
+ id,
572
+ index: this._getAlias(index, collection),
573
+ op_type: id ? "create" : "index",
574
+ refresh,
575
+ };
576
+ assertNoRouting(esRequest);
577
+ assertWellFormedRefresh(esRequest);
578
+ // Add metadata
579
+ if (injectKuzzleMeta) {
580
+ esRequest.document._kuzzle_info = {
581
+ author: getKuid(userId),
582
+ createdAt: Date.now(),
583
+ updatedAt: null,
584
+ updater: null,
585
+ };
586
+ }
587
+ (0, debug_1.default)("Create document: %o", esRequest);
588
+ try {
589
+ const body = await this._client.index(esRequest);
590
+ return {
591
+ _id: body._id,
592
+ _source: esRequest.document,
593
+ _version: body._version,
594
+ };
595
+ }
596
+ catch (error) {
597
+ throw this._esWrapper.formatESError(error);
598
+ }
599
+ }
600
+ /**
601
+ * Creates a new document to Elasticsearch, or replace it if it already exist
602
+ *
603
+ * @param {String} index - Index name
604
+ * @param {String} collection - Collection name
605
+ * @param {String} id - Document id
606
+ * @param {Object} content - Document content
607
+ * @param {Object} options - refresh (undefined), userId (null), injectKuzzleMeta (true)
608
+ *
609
+ * @returns {Promise.<Object>} { _id, _version, _source, created }
610
+ */
611
+ async createOrReplace(index, collection, id, content, { refresh, userId = null, injectKuzzleMeta = true, } = {}) {
612
+ const esRequest = {
613
+ document: content,
614
+ id,
615
+ index: this._getAlias(index, collection),
616
+ refresh,
617
+ };
618
+ assertNoRouting(esRequest);
619
+ assertWellFormedRefresh(esRequest);
620
+ // Add metadata
621
+ if (injectKuzzleMeta) {
622
+ esRequest.document._kuzzle_info = {
623
+ author: getKuid(userId),
624
+ createdAt: Date.now(),
625
+ updatedAt: Date.now(),
626
+ updater: getKuid(userId),
627
+ };
628
+ }
629
+ (0, debug_1.default)("Create or replace document: %o", esRequest);
630
+ try {
631
+ const body = await this._client.index(esRequest);
632
+ return {
633
+ _id: body._id,
634
+ _source: esRequest.document,
635
+ _version: body._version,
636
+ created: body.result === "created", // Needed by the notifier
637
+ };
638
+ }
639
+ catch (error) {
640
+ throw this._esWrapper.formatESError(error);
641
+ }
642
+ }
643
+ /**
644
+ * Sends the partial document to elasticsearch with the id to update
645
+ *
646
+ * @param {String} index - Index name
647
+ * @param {String} collection - Collection name
648
+ * @param {String} id - Document id
649
+ * @param {Object} content - Updated content
650
+ * @param {Object} options - refresh (undefined), userId (null), retryOnConflict (0)
651
+ *
652
+ * @returns {Promise.<{ _id, _version }>}
653
+ */
654
+ async update(index, collection, id, content, { refresh, userId = null, retryOnConflict, injectKuzzleMeta = true, } = {}) {
655
+ const esRequest = {
656
+ _source: true,
657
+ doc: content,
658
+ id,
659
+ index: this._getAlias(index, collection),
660
+ refresh,
661
+ retry_on_conflict: retryOnConflict || this._config.defaults.onUpdateConflictRetries,
662
+ };
663
+ assertNoRouting(esRequest);
664
+ assertWellFormedRefresh(esRequest);
665
+ if (injectKuzzleMeta) {
666
+ // Add metadata
667
+ esRequest.doc._kuzzle_info = {
668
+ ...esRequest.doc._kuzzle_info,
669
+ updatedAt: Date.now(),
670
+ updater: getKuid(userId),
671
+ };
672
+ }
673
+ (0, debug_1.default)("Update document: %o", esRequest);
674
+ try {
675
+ const body = await this._client.update(esRequest);
676
+ return {
677
+ _id: body._id,
678
+ _source: body.get._source,
679
+ _version: body._version,
680
+ };
681
+ }
682
+ catch (error) {
683
+ throw this._esWrapper.formatESError(error);
684
+ }
685
+ }
686
+ /**
687
+ * Sends the partial document to elasticsearch with the id to update
688
+ * Creates the document if it doesn't already exist
689
+ *
690
+ * @param {String} index - Index name
691
+ * @param {String} collection - Collection name
692
+ * @param {String} id - Document id
693
+ * @param {Object} content - Updated content
694
+ * @param {Object} options - defaultValues ({}), refresh (undefined), userId (null), retryOnConflict (0)
695
+ *
696
+ * @returns {Promise.<{ _id, _version }>}
697
+ */
698
+ async upsert(index, collection, id, content, { defaultValues = {}, refresh, userId = null, retryOnConflict, injectKuzzleMeta = true, } = {}) {
699
+ const esRequest = {
700
+ _source: true,
701
+ doc: content,
702
+ id,
703
+ index: this._getAlias(index, collection),
704
+ refresh,
705
+ retry_on_conflict: retryOnConflict || this._config.defaults.onUpdateConflictRetries,
706
+ upsert: { ...defaultValues, ...content },
707
+ };
708
+ assertNoRouting(esRequest);
709
+ assertWellFormedRefresh(esRequest);
710
+ // Add metadata
711
+ const user = getKuid(userId);
712
+ const now = Date.now();
713
+ if (injectKuzzleMeta) {
714
+ esRequest.doc._kuzzle_info = {
715
+ ...esRequest.doc._kuzzle_info,
716
+ updatedAt: now,
717
+ updater: user,
718
+ };
719
+ esRequest.upsert._kuzzle_info = {
720
+ ...esRequest.upsert._kuzzle_info,
721
+ author: user,
722
+ createdAt: now,
723
+ };
724
+ }
725
+ (0, debug_1.default)("Upsert document: %o", esRequest);
726
+ try {
727
+ const body = await this._client.update(esRequest);
728
+ return {
729
+ _id: body._id,
730
+ _source: body.get._source,
731
+ _version: body._version,
732
+ created: body.result === "created",
733
+ };
734
+ }
735
+ catch (error) {
736
+ throw this._esWrapper.formatESError(error);
737
+ }
738
+ }
739
+ /**
740
+ * Replaces a document to Elasticsearch
741
+ *
742
+ * @param {String} index - Index name
743
+ * @param {String} collection - Collection name
744
+ * @param {String} id - Document id
745
+ * @param {Object} content - Document content
746
+ * @param {Object} options - refresh (undefined), userId (null)
747
+ *
748
+ * @returns {Promise.<{ _id, _version, _source }>}
749
+ */
750
+ async replace(index, collection, id, content, { refresh, userId = null, injectKuzzleMeta = true, } = {}) {
751
+ const alias = this._getAlias(index, collection);
752
+ const esRequest = {
753
+ document: content,
754
+ id,
755
+ index: alias,
756
+ refresh,
757
+ };
758
+ assertNoRouting(esRequest);
759
+ assertWellFormedRefresh(esRequest);
760
+ if (injectKuzzleMeta) {
761
+ // Add metadata
762
+ esRequest.document._kuzzle_info = {
763
+ author: getKuid(userId),
764
+ createdAt: Date.now(),
765
+ updatedAt: Date.now(),
766
+ updater: getKuid(userId),
767
+ };
768
+ }
769
+ try {
770
+ const exists = await this._client.exists({ id, index: alias });
771
+ if (!exists) {
772
+ throw kerror.get("services", "storage", "not_found", id, index, collection);
773
+ }
774
+ (0, debug_1.default)("Replace document: %o", esRequest);
775
+ const body = await this._client.index(esRequest);
776
+ return {
777
+ _id: id,
778
+ _source: esRequest.document,
779
+ _version: body._version,
780
+ };
781
+ }
782
+ catch (error) {
783
+ throw this._esWrapper.formatESError(error);
784
+ }
785
+ }
786
+ /**
787
+ * Sends to elasticsearch the document id to delete
788
+ *
789
+ * @param {String} index - Index name
790
+ * @param {String} collection - Collection name
791
+ * @param {String} id - Document id
792
+ * @param {Object} options - refresh (undefined)
793
+ *
794
+ * @returns {Promise}
795
+ */
796
+ async delete(index, collection, id, { refresh, } = {}) {
797
+ const esRequest = {
798
+ id,
799
+ index: this._getAlias(index, collection),
800
+ refresh,
801
+ };
802
+ assertWellFormedRefresh(esRequest);
803
+ (0, debug_1.default)("Delete document: %o", esRequest);
804
+ try {
805
+ await this._client.delete(esRequest);
806
+ }
807
+ catch (error) {
808
+ throw this._esWrapper.formatESError(error);
809
+ }
810
+ return null;
811
+ }
812
+ /**
813
+ * Deletes all documents matching the provided filters.
814
+ * If fetch=false, the max documents write limit is not applied.
815
+ *
816
+ * Options:
817
+ * - size: size of the batch to retrieve documents (no-op if fetch=false)
818
+ * - refresh: refresh option for ES
819
+ * - fetch: if true, will fetch the documents before delete them
820
+ *
821
+ * @param {String} index - Index name
822
+ * @param {String} collection - Collection name
823
+ * @param {Object} query - Query to match documents
824
+ * @param {Object} options - size (undefined), refresh (undefined), fetch (true)
825
+ *
826
+ * @returns {Promise.<{ documents, total, deleted, failures: Array<{ id, reason }> }>}
827
+ */
828
+ async deleteByQuery(index, collection, query, { refresh, size = 1000, fetch = true, } = {}) {
829
+ const esRequest = {
830
+ ...this._sanitizeSearchBody({ query }),
831
+ index: this._getAlias(index, collection),
832
+ scroll: "5s",
833
+ };
834
+ if (!(0, safeObject_1.isPlainObject)(query)) {
835
+ throw kerror.get("services", "storage", "missing_argument", "body.query");
836
+ }
837
+ try {
838
+ let documents = [];
839
+ if (fetch) {
840
+ documents = await this._getAllDocumentsFromQuery({
841
+ ...esRequest,
842
+ size,
843
+ });
844
+ }
845
+ (0, debug_1.default)("Delete by query: %o", esRequest);
846
+ esRequest.refresh = refresh === "wait_for" ? true : refresh;
847
+ const request = {
848
+ ...esRequest,
849
+ max_docs: size,
850
+ };
851
+ if (request.max_docs === -1) {
852
+ request.max_docs = undefined;
853
+ }
854
+ const body = await this._client.deleteByQuery(request);
855
+ return {
856
+ deleted: body.deleted,
857
+ documents,
858
+ failures: body.failures.map(({ id, cause }) => ({
859
+ id,
860
+ reason: cause.reason,
861
+ })),
862
+ total: body.total,
863
+ };
864
+ }
865
+ catch (error) {
866
+ throw this._esWrapper.formatESError(error);
867
+ }
868
+ }
869
+ /**
870
+ * Delete fields of a document and replace it
871
+ *
872
+ * @param {String} index - Index name
873
+ * @param {String} collection - Collection name
874
+ * @param {String} id - Document id
875
+ * @param {Array} fields - Document fields to be removed
876
+ * @param {Object} options - refresh (undefined), userId (null)
877
+ *
878
+ * @returns {Promise.<{ _id, _version, _source }>}
879
+ */
880
+ async deleteFields(index, collection, id, fields, { refresh, userId = null, } = {}) {
881
+ const alias = this._getAlias(index, collection);
882
+ const esRequest = {
883
+ id,
884
+ index: alias,
885
+ };
886
+ try {
887
+ (0, debug_1.default)("DeleteFields document: %o", esRequest);
888
+ const body = await this._client.get(esRequest);
889
+ for (const field of fields) {
890
+ if (lodash_1.default.has(body._source, field)) {
891
+ lodash_1.default.set(body._source, field, undefined);
892
+ }
893
+ }
894
+ const updatedInfos = {
895
+ updatedAt: Date.now(),
896
+ updater: getKuid(userId),
897
+ };
898
+ if (typeof body._source._kuzzle_info === "object") {
899
+ body._source._kuzzle_info = {
900
+ ...body._source._kuzzle_info,
901
+ ...updatedInfos,
902
+ };
903
+ }
904
+ else {
905
+ body._source._kuzzle_info = updatedInfos;
906
+ }
907
+ const newEsRequest = {
908
+ document: body._source,
909
+ id,
910
+ index: alias,
911
+ refresh,
912
+ };
913
+ assertNoRouting(newEsRequest);
914
+ assertWellFormedRefresh(newEsRequest);
915
+ const updated = await this._client.index(newEsRequest);
916
+ return {
917
+ _id: id,
918
+ _source: body._source,
919
+ _version: updated._version,
920
+ };
921
+ }
922
+ catch (error) {
923
+ throw this._esWrapper.formatESError(error);
924
+ }
925
+ }
926
+ /**
927
+ * Updates all documents matching the provided filters
928
+ *
929
+ * @param {String} index - Index name
930
+ * @param {String} collection - Collection name
931
+ * @param {Object} query - Query to match documents
932
+ * @param {Object} changes - Changes wanted on documents
933
+ * @param {Object} options - refresh (undefined), size (undefined)
934
+ *
935
+ * @returns {Promise.<{ successes: [_id, _source, _status], errors: [ document, status, reason ] }>}
936
+ */
937
+ async updateByQuery(index, collection, query, changes, { refresh, size = 1000, userId = null, } = {}) {
938
+ try {
939
+ const esRequest = {
940
+ ...this._sanitizeSearchBody({ query }),
941
+ index: this._getAlias(index, collection),
942
+ scroll: "5s",
943
+ size,
944
+ };
945
+ const documents = await this._getAllDocumentsFromQuery(esRequest);
946
+ for (const document of documents) {
947
+ document._source = undefined;
948
+ document.body = changes;
949
+ }
950
+ (0, debug_1.default)("Update by query: %o", esRequest);
951
+ const { errors, items } = await this.mUpdate(index, collection, documents, { refresh, userId });
952
+ return {
953
+ errors,
954
+ successes: items,
955
+ };
956
+ }
957
+ catch (error) {
958
+ throw this._esWrapper.formatESError(error);
959
+ }
960
+ }
961
+ /**
962
+ * Updates all documents matching the provided filters
963
+ *
964
+ * @param {String} index - Index name
965
+ * @param {String} collection - Collection name
966
+ * @param {Object} query - Query to match documents
967
+ * @param {Object} changes - Changes wanted on documents
968
+ * @param {Object} options - refresh (undefined)
969
+ *
970
+ * @returns {Promise.<{ successes: [_id, _source, _status], errors: [ document, status, reason ] }>}
971
+ */
972
+ async bulkUpdateByQuery(index, collection, query, changes, { refresh = false, } = {}) {
973
+ const script = {
974
+ params: {},
975
+ source: "",
976
+ };
977
+ const flatChanges = (0, extractFields_1.default)(changes, { alsoExtractValues: true });
978
+ for (const { key, value } of flatChanges) {
979
+ script.source += `ctx._source.${key} = params['${key}'];`;
980
+ script.params[key] = value;
981
+ }
982
+ const esRequest = {
983
+ index: this._getAlias(index, collection),
984
+ query: this._sanitizeSearchBody({ query }).query,
985
+ refresh,
986
+ script,
987
+ };
988
+ (0, debug_1.default)("Bulk Update by query: %o", esRequest);
989
+ let response;
990
+ try {
991
+ response = await this._client.updateByQuery(esRequest);
992
+ }
993
+ catch (error) {
994
+ throw this._esWrapper.formatESError(error);
995
+ }
996
+ if (response.failures.length) {
997
+ const errors = response.failures.map(({ id, cause }) => ({
998
+ cause,
999
+ id,
1000
+ }));
1001
+ throw kerror.get("services", "storage", "incomplete_update", response.updated, errors);
1002
+ }
1003
+ return {
1004
+ updated: response.updated,
1005
+ };
1006
+ }
1007
+ /**
1008
+ * Execute the callback with a batch of documents of specified size until all
1009
+ * documents matched by the query have been processed.
1010
+ *
1011
+ * @param {String} index - Index name
1012
+ * @param {String} collection - Collection name
1013
+ * @param {Object} query - Query to match documents
1014
+ * @param {Function} callback - callback that will be called with the "hits" array
1015
+ * @param {Object} options - size (10), scrollTTL ('5s')
1016
+ *
1017
+ * @returns {Promise.<any[]>} Array of results returned by the callback
1018
+ */
1019
+ async mExecute(index, collection, query, callback, { size = 10, scrollTTl = "5s", } = {}) {
1020
+ const esRequest = {
1021
+ ...this._sanitizeSearchBody({ query }),
1022
+ from: 0,
1023
+ index: this._getAlias(index, collection),
1024
+ scroll: scrollTTl,
1025
+ size,
1026
+ };
1027
+ if (!(0, safeObject_1.isPlainObject)(query)) {
1028
+ throw kerror.get("services", "storage", "missing_argument", "body.query");
1029
+ }
1030
+ const results = [];
1031
+ let processed = 0;
1032
+ let scrollId = null;
1033
+ try {
1034
+ let body = await this._client.search(esRequest);
1035
+ const totalHitsValue = this._getHitsTotalValue(body.hits);
1036
+ while (processed < totalHitsValue && body.hits.hits.length > 0) {
1037
+ scrollId = body._scroll_id;
1038
+ results.push(await callback(body.hits.hits));
1039
+ processed += body.hits.hits.length;
1040
+ body = await this._client.scroll({
1041
+ scroll: esRequest.scroll,
1042
+ scroll_id: scrollId,
1043
+ });
1044
+ }
1045
+ }
1046
+ finally {
1047
+ await this.clearScroll(scrollId);
1048
+ }
1049
+ return results;
1050
+ }
1051
+ /**
1052
+ * Creates a new index.
1053
+ *
1054
+ * This methods creates an hidden collection in the provided index to be
1055
+ * able to list it.
1056
+ * This methods resolves if the index name does not already exists either as
1057
+ * private or public index.
1058
+ *
1059
+ * @param {String} index - Index name
1060
+ *
1061
+ * @returns {Promise}
1062
+ */
1063
+ async createIndex(index) {
1064
+ this._assertValidIndexAndCollection(index);
1065
+ let body;
1066
+ try {
1067
+ body = await this._client.cat.aliases({ format: "json" });
1068
+ }
1069
+ catch (error) {
1070
+ throw this._esWrapper.formatESError(error);
1071
+ }
1072
+ const aliases = body.map(({ alias: name }) => name);
1073
+ for (const alias of aliases) {
1074
+ const indexName = this._extractIndex(alias);
1075
+ if (index === indexName) {
1076
+ const indexType = alias[INDEX_PREFIX_POSITION_IN_ALIAS] === PRIVATE_PREFIX
1077
+ ? "private"
1078
+ : "public";
1079
+ throw kerror.get("services", "storage", "index_already_exists", indexType, index);
1080
+ }
1081
+ }
1082
+ await this._createHiddenCollection(index);
1083
+ return null;
1084
+ }
1085
+ /**
1086
+ * Creates an empty collection.
1087
+ * Mappings and settings will be applied if supplied.
1088
+ *
1089
+ * @param {String} index - Index name
1090
+ * @param {String} collection - Collection name
1091
+ * @param {Object} config - mappings ({}), settings ({})
1092
+ *
1093
+ * @returns {Promise}
1094
+ */
1095
+ async createCollection(index, collection, { mappings = {}, settings = {}, } = {}) {
1096
+ this._assertValidIndexAndCollection(index, collection);
1097
+ if (collection === HIDDEN_COLLECTION) {
1098
+ throw kerror.get("services", "storage", "collection_reserved", HIDDEN_COLLECTION);
1099
+ }
1100
+ const mutex = new mutex_1.Mutex(`hiddenCollection/create/${index}`);
1101
+ try {
1102
+ await mutex.lock();
1103
+ if (await this._hasHiddenCollection(index)) {
1104
+ await this.deleteCollection(index, HIDDEN_COLLECTION);
1105
+ }
1106
+ }
1107
+ catch (error) {
1108
+ throw this._esWrapper.formatESError(error);
1109
+ }
1110
+ finally {
1111
+ await mutex.unlock();
1112
+ }
1113
+ const esRequest = {
1114
+ aliases: {
1115
+ [this._getAlias(index, collection)]: {},
1116
+ },
1117
+ index: await this._getAvailableIndice(index, collection),
1118
+ mappings: {},
1119
+ settings,
1120
+ wait_for_active_shards: await this._getWaitForActiveShards(),
1121
+ };
1122
+ this._checkDynamicProperty(mappings);
1123
+ const exists = await this.hasCollection(index, collection);
1124
+ if (exists) {
1125
+ return this.updateCollection(index, collection, { mappings, settings });
1126
+ }
1127
+ this._checkMappings(mappings);
1128
+ esRequest.mappings = {
1129
+ _meta: mappings._meta || this._config.commonMapping._meta,
1130
+ dynamic: mappings.dynamic || this._config.commonMapping.dynamic,
1131
+ properties: lodash_1.default.merge(mappings.properties, this._config.commonMapping.properties),
1132
+ };
1133
+ esRequest.settings.number_of_replicas =
1134
+ esRequest.settings.number_of_replicas ||
1135
+ this._config.defaultSettings.number_of_replicas;
1136
+ esRequest.settings.number_of_shards =
1137
+ esRequest.settings.number_of_shards ||
1138
+ this._config.defaultSettings.number_of_shards;
1139
+ try {
1140
+ await this._client.indices.create(esRequest);
1141
+ }
1142
+ catch (error) {
1143
+ if (lodash_1.default.get(error, "meta.body.error.type") ===
1144
+ "resource_already_exists_exception") {
1145
+ // race condition: the indice has been created between the "exists"
1146
+ // check above and this "create" attempt
1147
+ return null;
1148
+ }
1149
+ throw this._esWrapper.formatESError(error);
1150
+ }
1151
+ return null;
1152
+ }
1153
+ /**
1154
+ * Retrieves settings definition for index/type
1155
+ *
1156
+ * @param {String} index - Index name
1157
+ * @param {String} collection - Collection name
1158
+ *
1159
+ * @returns {Promise.<{ settings }>}
1160
+ */
1161
+ async getSettings(index, collection) {
1162
+ const indice = await this._getIndice(index, collection);
1163
+ const esRequest = {
1164
+ index: indice,
1165
+ };
1166
+ (0, debug_1.default)("Get settings: %o", esRequest);
1167
+ try {
1168
+ const body = await this._client.indices.getSettings(esRequest);
1169
+ return body[indice].settings.index;
1170
+ }
1171
+ catch (error) {
1172
+ throw this._esWrapper.formatESError(error);
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Retrieves mapping definition for index/type
1177
+ *
1178
+ * @param {String} index - Index name
1179
+ * @param {String} collection - Collection name
1180
+ * @param {Object} options - includeKuzzleMeta (false)
1181
+ *
1182
+ * @returns {Promise.<{ dynamic, _meta, properties }>}
1183
+ */
1184
+ async getMapping(index, collection, { includeKuzzleMeta = false, } = {}) {
1185
+ const indice = await this._getIndice(index, collection);
1186
+ const esRequest = {
1187
+ index: indice,
1188
+ };
1189
+ (0, debug_1.default)("Get mapping: %o", esRequest);
1190
+ try {
1191
+ const body = await this._client.indices.getMapping(esRequest);
1192
+ const properties = includeKuzzleMeta
1193
+ ? body[indice].mappings.properties
1194
+ : lodash_1.default.omit(body[indice].mappings.properties, "_kuzzle_info");
1195
+ return {
1196
+ _meta: body[indice].mappings._meta,
1197
+ dynamic: body[indice].mappings.dynamic,
1198
+ properties,
1199
+ };
1200
+ }
1201
+ catch (error) {
1202
+ throw this._esWrapper.formatESError(error);
1203
+ }
1204
+ }
1205
+ /**
1206
+ * Updates a collection mappings and settings
1207
+ *
1208
+ * @param {String} index - Index name
1209
+ * @param {String} collection - Collection name
1210
+ * @param {Object} config - mappings ({}), settings ({})
1211
+ *
1212
+ * @returns {Promise}
1213
+ */
1214
+ async updateCollection(index, collection, { mappings = {}, settings = {}, } = {}) {
1215
+ const esRequest = {
1216
+ index: await this._getIndice(index, collection),
1217
+ };
1218
+ // If either the putMappings or the putSettings operation fail, we need to
1219
+ // rollback the whole operation. Since mappings can't be rollback, we try to
1220
+ // update the settings first, then the mappings and we rollback the settings
1221
+ // if putMappings fail.
1222
+ let indexSettings;
1223
+ try {
1224
+ indexSettings = await this._getSettings(esRequest);
1225
+ }
1226
+ catch (error) {
1227
+ throw this._esWrapper.formatESError(error);
1228
+ }
1229
+ if (!lodash_1.default.isEmpty(settings)) {
1230
+ await this.updateSettings(index, collection, settings);
1231
+ }
1232
+ try {
1233
+ if (!lodash_1.default.isEmpty(mappings)) {
1234
+ const previousMappings = await this.getMapping(index, collection, {
1235
+ includeKuzzleMeta: true,
1236
+ });
1237
+ await this.updateMapping(index, collection, mappings);
1238
+ if (this._dynamicChanges(previousMappings, mappings)) {
1239
+ await this.updateSearchIndex(index, collection);
1240
+ }
1241
+ }
1242
+ }
1243
+ catch (error) {
1244
+ const allowedSettings = this.getAllowedIndexSettings(indexSettings);
1245
+ // Rollback to previous settings
1246
+ if (!lodash_1.default.isEmpty(settings)) {
1247
+ await this.updateSettings(index, collection, allowedSettings);
1248
+ }
1249
+ throw error;
1250
+ }
1251
+ return null;
1252
+ }
1253
+ /**
1254
+ * Given index settings we return a new version of index settings
1255
+ * only with allowed settings that can be set (during update or create index).
1256
+ * @param indexSettings the index settings
1257
+ * @returns {{index: *}} a new index settings with only allowed settings.
1258
+ */
1259
+ getAllowedIndexSettings(indexSettings) {
1260
+ return {
1261
+ index: lodash_1.default.omit(indexSettings.index, [
1262
+ "creation_date",
1263
+ "provided_name",
1264
+ "uuid",
1265
+ "version",
1266
+ ]),
1267
+ };
1268
+ }
1269
+ /**
1270
+ * Sends an empty UpdateByQuery request to update the search index
1271
+ *
1272
+ * @param {String} index - Index name
1273
+ * @param {String} collection - Collection name
1274
+ * @returns {Promise.<Object>} {}
1275
+ */
1276
+ async updateSearchIndex(index, collection) {
1277
+ const esRequest = {
1278
+ // @cluster: conflicts when two nodes start at the same time
1279
+ conflicts: "proceed",
1280
+ index: this._getAlias(index, collection),
1281
+ refresh: true,
1282
+ // This operation can take some time: this should be an ES
1283
+ // background task. And it's preferable to a request timeout when
1284
+ // processing large indexes.
1285
+ wait_for_completion: false,
1286
+ };
1287
+ (0, debug_1.default)("UpdateByQuery: %o", esRequest);
1288
+ try {
1289
+ await this._client.updateByQuery(esRequest);
1290
+ }
1291
+ catch (error) {
1292
+ throw this._esWrapper.formatESError(error);
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Update a collection mappings
1297
+ *
1298
+ * @param {String} index - Index name
1299
+ * @param {String} collection - Collection name
1300
+ * @param {Object} mappings - Collection mappings in ES format
1301
+ *
1302
+ * @returns {Promise.<{ dynamic, _meta, properties }>}
1303
+ */
1304
+ async updateMapping(index, collection, mappings = {}) {
1305
+ let esRequest = {
1306
+ index: this._getAlias(index, collection),
1307
+ };
1308
+ this._checkDynamicProperty(mappings);
1309
+ const collectionMappings = await this.getMapping(index, collection, {
1310
+ includeKuzzleMeta: true,
1311
+ });
1312
+ this._checkMappings(mappings);
1313
+ esRequest = {
1314
+ ...esRequest,
1315
+ _meta: mappings._meta || collectionMappings._meta,
1316
+ dynamic: mappings.dynamic || collectionMappings.dynamic,
1317
+ properties: mappings.properties,
1318
+ };
1319
+ (0, debug_1.default)("Update mapping: %o", esRequest);
1320
+ try {
1321
+ await this._client.indices.putMapping(esRequest);
1322
+ }
1323
+ catch (error) {
1324
+ throw this._esWrapper.formatESError(error);
1325
+ }
1326
+ const fullProperties = lodash_1.default.merge(collectionMappings.properties, mappings.properties);
1327
+ return {
1328
+ _meta: esRequest._meta,
1329
+ dynamic: esRequest.dynamic.toString(),
1330
+ properties: fullProperties,
1331
+ };
1332
+ }
1333
+ /**
1334
+ * Updates a collection settings (eg: analyzers)
1335
+ *
1336
+ * @param {String} index - Index name
1337
+ * @param {String} collection - Collection name
1338
+ * @param {Object} settings - Collection settings in ES format
1339
+ *
1340
+ * @returns {Promise}
1341
+ */
1342
+ async updateSettings(index, collection, settings = {}) {
1343
+ const esRequest = {
1344
+ index: this._getAlias(index, collection),
1345
+ };
1346
+ await this._client.indices.close(esRequest);
1347
+ try {
1348
+ await this._client.indices.putSettings({ ...esRequest, body: settings });
1349
+ }
1350
+ catch (error) {
1351
+ throw this._esWrapper.formatESError(error);
1352
+ }
1353
+ finally {
1354
+ await this._client.indices.open(esRequest);
1355
+ }
1356
+ return null;
1357
+ }
1358
+ /**
1359
+ * Empties the content of a collection. Keep the existing mapping and settings.
1360
+ *
1361
+ * @param {String} index - Index name
1362
+ * @param {String} collection - Collection name
1363
+ *
1364
+ * @returns {Promise}
1365
+ */
1366
+ async truncateCollection(index, collection) {
1367
+ let mappings;
1368
+ let settings;
1369
+ const esRequest = {
1370
+ index: await this._getIndice(index, collection),
1371
+ };
1372
+ try {
1373
+ mappings = await this.getMapping(index, collection, {
1374
+ includeKuzzleMeta: true,
1375
+ });
1376
+ settings = await this._getSettings(esRequest);
1377
+ settings = {
1378
+ ...settings,
1379
+ ...this.getAllowedIndexSettings(settings),
1380
+ };
1381
+ await this._client.indices.delete(esRequest);
1382
+ await this._client.indices.create({
1383
+ ...esRequest,
1384
+ aliases: {
1385
+ [this._getAlias(index, collection)]: {},
1386
+ },
1387
+ mappings,
1388
+ settings,
1389
+ wait_for_active_shards: await this._getWaitForActiveShards(),
1390
+ });
1391
+ return null;
1392
+ }
1393
+ catch (error) {
1394
+ throw this._esWrapper.formatESError(error);
1395
+ }
1396
+ }
1397
+ /**
1398
+ * Runs several action and document
1399
+ *
1400
+ * @param {String} index - Index name
1401
+ * @param {String} collection - Collection name
1402
+ * @param {Object[]} documents - Documents to import
1403
+ * @param {Object} options - timeout (undefined), refresh (undefined), userId (null)
1404
+ *
1405
+ * @returns {Promise.<{ items, errors }>
1406
+ */
1407
+ async import(index, collection, documents, { refresh, timeout, userId = null, } = {}) {
1408
+ const alias = this._getAlias(index, collection);
1409
+ const dateNow = Date.now();
1410
+ const esRequest = {
1411
+ operations: documents,
1412
+ refresh,
1413
+ timeout,
1414
+ };
1415
+ const kuzzleMeta = {
1416
+ created: {
1417
+ author: getKuid(userId),
1418
+ createdAt: dateNow,
1419
+ updatedAt: null,
1420
+ updater: null,
1421
+ },
1422
+ updated: {
1423
+ updatedAt: dateNow,
1424
+ updater: getKuid(userId),
1425
+ },
1426
+ };
1427
+ assertWellFormedRefresh(esRequest);
1428
+ this._scriptCheck(documents);
1429
+ this._setLastActionToKuzzleMeta(esRequest, alias, kuzzleMeta);
1430
+ let body;
1431
+ try {
1432
+ body = await this._client.bulk(esRequest);
1433
+ }
1434
+ catch (error) {
1435
+ throw this._esWrapper.formatESError(error);
1436
+ }
1437
+ const result = {
1438
+ errors: [],
1439
+ items: [],
1440
+ };
1441
+ let idx = 0;
1442
+ /**
1443
+ * @warning Critical code section
1444
+ *
1445
+ * bulk body can contain more than 10K elements
1446
+ */
1447
+ for (let i = 0; i < body.items.length; i++) {
1448
+ const row = body.items[i];
1449
+ const action = Object.keys(row)[0];
1450
+ const item = row[action];
1451
+ if (item.status >= 400) {
1452
+ const error = {
1453
+ _id: item._id,
1454
+ status: item.status,
1455
+ };
1456
+ // update action contain body in "doc" field
1457
+ // the delete action is not followed by an action payload
1458
+ if (action === "update") {
1459
+ error._source = documents[idx + 1].doc;
1460
+ error._source._kuzzle_info = undefined;
1461
+ }
1462
+ else if (action !== "delete") {
1463
+ error._source = documents[idx + 1];
1464
+ error._source._kuzzle_info = undefined;
1465
+ }
1466
+ // ES response does not systematicaly include an error object
1467
+ // (e.g. delete action with 404 status)
1468
+ if (item.error) {
1469
+ error.error = {
1470
+ reason: item.error.reason,
1471
+ type: item.error.type,
1472
+ };
1473
+ }
1474
+ result.errors.push({ [action]: error });
1475
+ }
1476
+ else {
1477
+ result.items.push({
1478
+ [action]: {
1479
+ _id: item._id,
1480
+ status: item.status,
1481
+ },
1482
+ });
1483
+ }
1484
+ // the delete action is not followed by an action payload
1485
+ idx = action === "delete" ? idx + 1 : idx + 2;
1486
+ }
1487
+ /* end critical code section */
1488
+ return result;
1489
+ }
1490
+ /**
1491
+ * Retrieves the complete list of existing collections in the current index
1492
+ *
1493
+ * @param {String} index - Index name
1494
+ * @param {Object.Boolean} includeHidden - Optional: include HIDDEN_COLLECTION in results
1495
+ *
1496
+ * @returns {Promise.<Array>} Collection names
1497
+ */
1498
+ async listCollections(index, { includeHidden = false } = {}) {
1499
+ let body;
1500
+ try {
1501
+ body = await this._client.cat.aliases({ format: "json" });
1502
+ }
1503
+ catch (error) {
1504
+ throw this._esWrapper.formatESError(error);
1505
+ }
1506
+ const aliases = body.map(({ alias }) => alias);
1507
+ const schema = this._extractSchema(aliases, { includeHidden });
1508
+ return schema[index] || [];
1509
+ }
1510
+ /**
1511
+ * Retrieves the complete list of indexes
1512
+ *
1513
+ * @returns {Promise.<Array>} Index names
1514
+ */
1515
+ async listIndexes() {
1516
+ let body;
1517
+ try {
1518
+ body = await this._client.cat.aliases({ format: "json" });
1519
+ }
1520
+ catch (error) {
1521
+ throw this._esWrapper.formatESError(error);
1522
+ }
1523
+ const aliases = body.map(({ alias }) => alias);
1524
+ const schema = this._extractSchema(aliases);
1525
+ return Object.keys(schema);
1526
+ }
1527
+ /**
1528
+ * Returns an object containing the list of indexes and collections
1529
+ *
1530
+ * @returns {Object.<String, String[]>} Object<index, collections>
1531
+ */
1532
+ async getSchema() {
1533
+ let body;
1534
+ try {
1535
+ body = await this._client.cat.aliases({ format: "json" });
1536
+ }
1537
+ catch (error) {
1538
+ throw this._esWrapper.formatESError(error);
1539
+ }
1540
+ const aliases = body.map(({ alias }) => alias);
1541
+ const schema = this._extractSchema(aliases, { includeHidden: true });
1542
+ for (const [index, collections] of Object.entries(schema)) {
1543
+ schema[index] = collections.filter((c) => c !== HIDDEN_COLLECTION);
1544
+ }
1545
+ return schema;
1546
+ }
1547
+ /**
1548
+ * Retrieves the complete list of aliases
1549
+ *
1550
+ * @returns {Promise.<Object[]>} [ { alias, index, collection, indice } ]
1551
+ */
1552
+ async listAliases() {
1553
+ let body;
1554
+ try {
1555
+ body = await this._client.cat.aliases({ format: "json" });
1556
+ }
1557
+ catch (error) {
1558
+ throw this._esWrapper.formatESError(error);
1559
+ }
1560
+ const aliases = [];
1561
+ for (const { alias, index: indice } of body) {
1562
+ if (alias[INDEX_PREFIX_POSITION_IN_ALIAS] === this._indexPrefix) {
1563
+ aliases.push({
1564
+ alias,
1565
+ collection: this._extractCollection(alias),
1566
+ index: this._extractIndex(alias),
1567
+ indice,
1568
+ });
1569
+ }
1570
+ }
1571
+ return aliases;
1572
+ }
1573
+ /**
1574
+ * Deletes a collection
1575
+ *
1576
+ * @param {String} index - Index name
1577
+ * @param {String} collection - Collection name
1578
+ *
1579
+ * @returns {Promise}
1580
+ */
1581
+ async deleteCollection(index, collection) {
1582
+ const indice = await this._getIndice(index, collection);
1583
+ const esRequest = {
1584
+ index: indice,
1585
+ };
1586
+ try {
1587
+ await this._client.indices.delete(esRequest);
1588
+ const alias = this._getAlias(index, collection);
1589
+ if (await this._checkIfAliasExists(alias)) {
1590
+ await this._client.indices.deleteAlias({
1591
+ index: indice,
1592
+ name: alias,
1593
+ });
1594
+ }
1595
+ await this._createHiddenCollection(index);
1596
+ }
1597
+ catch (e) {
1598
+ throw this._esWrapper.formatESError(e);
1599
+ }
1600
+ return null;
1601
+ }
1602
+ /**
1603
+ * Deletes multiple indexes
1604
+ *
1605
+ * @param {String[]} indexes - Index names
1606
+ *
1607
+ * @returns {Promise.<String[]>}
1608
+ */
1609
+ async deleteIndexes(indexes = []) {
1610
+ if (indexes.length === 0) {
1611
+ return bluebird_1.default.resolve([]);
1612
+ }
1613
+ const deleted = new Set();
1614
+ try {
1615
+ const body = await this._client.cat.aliases({ format: "json" });
1616
+ const esRequest = body.reduce((request, { alias, index: indice }) => {
1617
+ const index = this._extractIndex(alias);
1618
+ if (alias[INDEX_PREFIX_POSITION_IN_ALIAS] !== this._indexPrefix ||
1619
+ !indexes.includes(index)) {
1620
+ return request;
1621
+ }
1622
+ deleted.add(index);
1623
+ request.index.push(indice);
1624
+ return request;
1625
+ }, { index: [] });
1626
+ if (esRequest.index.length === 0) {
1627
+ return [];
1628
+ }
1629
+ (0, debug_1.default)("Delete indexes: %o", esRequest);
1630
+ await this._client.indices.delete(esRequest);
1631
+ }
1632
+ catch (error) {
1633
+ throw this._esWrapper.formatESError(error);
1634
+ }
1635
+ return Array.from(deleted);
1636
+ }
1637
+ /**
1638
+ * Deletes an index
1639
+ *
1640
+ * @param {String} index - Index name
1641
+ *
1642
+ * @returns {Promise}
1643
+ */
1644
+ async deleteIndex(index) {
1645
+ await this.deleteIndexes([index]);
1646
+ return null;
1647
+ }
1648
+ /**
1649
+ * Forces a refresh on the collection.
1650
+ *
1651
+ * /!\ Can lead to some performance issues.
1652
+ * cf https://www.elastic.co/guide/en/elasticsearch/guide/current/near-real-time.html for more details
1653
+ *
1654
+ * @param {String} index - Index name
1655
+ * @param {String} collection - Collection name
1656
+ *
1657
+ * @returns {Promise.<Object>} { _shards }
1658
+ */
1659
+ async refreshCollection(index, collection) {
1660
+ const esRequest = {
1661
+ index: this._getAlias(index, collection),
1662
+ };
1663
+ let body;
1664
+ try {
1665
+ body = await this._client.indices.refresh(esRequest);
1666
+ }
1667
+ catch (error) {
1668
+ throw this._esWrapper.formatESError(error);
1669
+ }
1670
+ return body;
1671
+ }
1672
+ /**
1673
+ * Returns true if the document exists
1674
+ *
1675
+ * @param {String} index - Index name
1676
+ * @param {String} collection - Collection name
1677
+ * @param {String} id - Document ID
1678
+ *
1679
+ * @returns {Promise.<boolean>}
1680
+ */
1681
+ async exists(index, collection, id) {
1682
+ const esRequest = {
1683
+ id,
1684
+ index: this._getAlias(index, collection),
1685
+ };
1686
+ try {
1687
+ return await this._client.exists(esRequest);
1688
+ }
1689
+ catch (error) {
1690
+ throw this._esWrapper.formatESError(error);
1691
+ }
1692
+ }
1693
+ /**
1694
+ * Returns the list of documents existing with the ids given in the body param
1695
+ * NB: Due to internal Kuzzle mechanism, can only be called on a single
1696
+ * index/collection, using the body { ids: [.. } syntax.
1697
+ *
1698
+ * @param {String} index - Index name
1699
+ * @param {String} collection - Collection name
1700
+ * @param {Array.<String>} ids - Document IDs
1701
+ *
1702
+ * @returns {Promise.<{ items: Array<{ _id, _source, _version }>, errors }>}
1703
+ */
1704
+ async mExists(index, collection, ids) {
1705
+ if (ids.length === 0) {
1706
+ return { errors: [], item: [] };
1707
+ }
1708
+ const esRequest = {
1709
+ _source: "false",
1710
+ docs: ids.map((_id) => ({ _id })),
1711
+ index: this._getAlias(index, collection),
1712
+ };
1713
+ (0, debug_1.default)("mExists: %o", esRequest);
1714
+ let body;
1715
+ try {
1716
+ body = await this._client.mget(esRequest); // NOSONAR
1717
+ }
1718
+ catch (e) {
1719
+ throw this._esWrapper.formatESError(e);
1720
+ }
1721
+ const errors = [];
1722
+ const items = [];
1723
+ for (let i = 0; i < body.docs.length; i++) {
1724
+ const doc = body.docs[i];
1725
+ if (!("error" in doc) && doc.found) {
1726
+ items.push(doc._id);
1727
+ }
1728
+ else {
1729
+ errors.push(doc._id);
1730
+ }
1731
+ }
1732
+ return { errors, items };
1733
+ }
1734
+ /**
1735
+ * Returns true if the index exists
1736
+ *
1737
+ * @param {String} index - Index name
1738
+ *
1739
+ * @returns {Promise.<boolean>}
1740
+ */
1741
+ async hasIndex(index) {
1742
+ const indexes = await this.listIndexes();
1743
+ return indexes.some((idx) => idx === index);
1744
+ }
1745
+ /**
1746
+ * Returns true if the collection exists
1747
+ *
1748
+ * @param {String} index - Index name
1749
+ * @param {String} collection - Collection name
1750
+ *
1751
+ * @returns {Promise.<boolean>}
1752
+ */
1753
+ async hasCollection(index, collection) {
1754
+ const collections = await this.listCollections(index);
1755
+ return collections.some((col) => col === collection);
1756
+ }
1757
+ /**
1758
+ * Returns true if the index has the hidden collection
1759
+ *
1760
+ * @param {String} index - Index name
1761
+ *
1762
+ * @returns {Promise.<boolean>}
1763
+ */
1764
+ async _hasHiddenCollection(index) {
1765
+ const collections = await this.listCollections(index, {
1766
+ includeHidden: true,
1767
+ });
1768
+ return collections.some((col) => col === HIDDEN_COLLECTION);
1769
+ }
1770
+ /**
1771
+ * Creates multiple documents at once.
1772
+ * If a content has no id, one is automatically generated and assigned to it.
1773
+ * If a content has a specified identifier, it is rejected if it already exists
1774
+ *
1775
+ * @param {String} index - Index name
1776
+ * @param {String} collection - Collection name
1777
+ * @param {Object[]} documents - Documents
1778
+ * @param {Object} options - timeout (undefined), refresh (undefined), userId (null)
1779
+ *
1780
+ * @returns {Promise.<Object>} { items, errors }
1781
+ */
1782
+ async mCreate(index, collection, documents, { refresh, timeout, userId = null, } = {}) {
1783
+ const alias = this._getAlias(index, collection), kuzzleMeta = {
1784
+ _kuzzle_info: {
1785
+ author: getKuid(userId),
1786
+ createdAt: Date.now(),
1787
+ updatedAt: null,
1788
+ updater: null,
1789
+ },
1790
+ }, { rejected, extractedDocuments, documentsToGet } = this._extractMDocuments(documents, kuzzleMeta, { prepareMGet: true });
1791
+ // prepare the mget request, but only for document having a specified id
1792
+ const body = documentsToGet.length > 0
1793
+ ? await this._client.mget({
1794
+ docs: documentsToGet,
1795
+ index: alias,
1796
+ })
1797
+ : { docs: [] };
1798
+ const existingDocuments = body.docs;
1799
+ const esRequest = {
1800
+ index: alias,
1801
+ operations: [],
1802
+ refresh,
1803
+ timeout,
1804
+ };
1805
+ const toImport = [];
1806
+ /**
1807
+ * @warning Critical code section
1808
+ *
1809
+ * request can contain more than 10K elements
1810
+ */
1811
+ for (let i = 0, idx = 0; i < extractedDocuments.length; i++) {
1812
+ const document = extractedDocuments[i];
1813
+ // Documents are retrieved in the same order than we got them from user
1814
+ if (typeof document._id === "string" && existingDocuments[idx]) {
1815
+ const doc = existingDocuments[idx];
1816
+ if (!("error" in doc) && doc.found) {
1817
+ document._source._kuzzle_info = undefined;
1818
+ rejected.push({
1819
+ document: {
1820
+ _id: document._id,
1821
+ body: document._source,
1822
+ },
1823
+ reason: "document already exists",
1824
+ status: 400,
1825
+ });
1826
+ }
1827
+ else {
1828
+ esRequest.operations.push({
1829
+ index: {
1830
+ _id: document._id,
1831
+ _index: alias,
1832
+ },
1833
+ });
1834
+ esRequest.operations.push(document._source);
1835
+ toImport.push(document);
1836
+ }
1837
+ idx++;
1838
+ }
1839
+ else {
1840
+ esRequest.operations.push({ index: { _index: alias } });
1841
+ esRequest.operations.push(document._source);
1842
+ toImport.push(document);
1843
+ }
1844
+ }
1845
+ /* end critical code section */
1846
+ return this._mExecute(esRequest, toImport, rejected);
1847
+ }
1848
+ /**
1849
+ * Creates or replaces multiple documents at once.
1850
+ *
1851
+ * @param {String} index - Index name
1852
+ * @param {String} collection - Collection name
1853
+ * @param {Object[]} documents - Documents
1854
+ * @param {Object} options - timeout (undefined), refresh (undefined), userId (null), injectKuzzleMeta (false), limits (true)
1855
+ *
1856
+ * @returns {Promise.<{ items, errors }>
1857
+ */
1858
+ async mCreateOrReplace(index, collection, documents, { refresh, timeout, userId = null, injectKuzzleMeta = true, limits = true, source = true, } = {}) {
1859
+ let kuzzleMeta = {};
1860
+ if (injectKuzzleMeta) {
1861
+ kuzzleMeta = {
1862
+ _kuzzle_info: {
1863
+ author: getKuid(userId),
1864
+ createdAt: Date.now(),
1865
+ updatedAt: null,
1866
+ updater: null,
1867
+ },
1868
+ };
1869
+ }
1870
+ const alias = this._getAlias(index, collection);
1871
+ const esRequest = {
1872
+ index: alias,
1873
+ operations: [],
1874
+ refresh,
1875
+ timeout,
1876
+ };
1877
+ const { rejected, extractedDocuments } = this._extractMDocuments(documents, kuzzleMeta);
1878
+ esRequest.operations = [];
1879
+ /**
1880
+ * @warning Critical code section
1881
+ *
1882
+ * request can contain more than 10K elements
1883
+ */
1884
+ for (let i = 0; i < extractedDocuments.length; i++) {
1885
+ esRequest.operations.push({
1886
+ index: {
1887
+ _id: extractedDocuments[i]._id,
1888
+ _index: alias,
1889
+ },
1890
+ });
1891
+ esRequest.operations.push(extractedDocuments[i]._source);
1892
+ }
1893
+ /* end critical code section */
1894
+ return this._mExecute(esRequest, extractedDocuments, rejected, {
1895
+ limits,
1896
+ source,
1897
+ });
1898
+ }
1899
+ /**
1900
+ * Updates multiple documents with one request
1901
+ * Replacements are rejected if targeted documents do not exist
1902
+ * (like with the normal "update" method)
1903
+ *
1904
+ * @param {String} index - Index name
1905
+ * @param {String} collection - Collection name
1906
+ * @param {Object[]} documents - Documents
1907
+ * @param {Object} options - timeout (undefined), refresh (undefined), retryOnConflict (0), userId (null)
1908
+ *
1909
+ * @returns {Promise.<Object>} { items, errors }
1910
+ */
1911
+ async mUpdate(index, collection, documents, { refresh = undefined, retryOnConflict = 0, timeout = undefined, userId = null, } = {}) {
1912
+ const alias = this._getAlias(index, collection), toImport = [], esRequest = {
1913
+ index: alias,
1914
+ operations: [],
1915
+ refresh,
1916
+ timeout,
1917
+ }, kuzzleMeta = {
1918
+ _kuzzle_info: {
1919
+ updatedAt: Date.now(),
1920
+ updater: getKuid(userId),
1921
+ },
1922
+ }, { rejected, extractedDocuments } = this._extractMDocuments(documents, kuzzleMeta);
1923
+ /**
1924
+ * @warning Critical code section
1925
+ *
1926
+ * request can contain more than 10K elements
1927
+ */
1928
+ for (let i = 0; i < extractedDocuments.length; i++) {
1929
+ const extractedDocument = extractedDocuments[i];
1930
+ if (typeof extractedDocument._id === "string") {
1931
+ esRequest.operations.push({
1932
+ update: {
1933
+ _id: extractedDocument._id,
1934
+ _index: alias,
1935
+ retry_on_conflict: retryOnConflict || this._config.defaults.onUpdateConflictRetries,
1936
+ },
1937
+ });
1938
+ // _source: true => makes ES return the updated document source in the
1939
+ // response. Required by the real-time notifier component
1940
+ esRequest.operations.push({
1941
+ _source: true,
1942
+ doc: extractedDocument._source,
1943
+ });
1944
+ toImport.push(extractedDocument);
1945
+ }
1946
+ else {
1947
+ extractedDocument._source._kuzzle_info = undefined;
1948
+ rejected.push({
1949
+ document: {
1950
+ _id: extractedDocument._id,
1951
+ body: extractedDocument._source,
1952
+ },
1953
+ reason: "document _id must be a string",
1954
+ status: 400,
1955
+ });
1956
+ }
1957
+ }
1958
+ /* end critical code section */
1959
+ const response = await this._mExecute(esRequest, toImport, rejected);
1960
+ // with _source: true, ES returns the updated document in
1961
+ // response.result.get._source
1962
+ // => we replace response.result._source with it so that the notifier
1963
+ // module can seamlessly process all kind of m* response*
1964
+ response.items = response.items.map((item) => ({
1965
+ _id: item._id,
1966
+ _source: item.get._source,
1967
+ _version: item._version,
1968
+ status: item.status,
1969
+ }));
1970
+ return response;
1971
+ }
1972
+ /**
1973
+ * Creates or replaces multiple documents at once.
1974
+ *
1975
+ * @param {String} index - Index name
1976
+ * @param {String} collection - Collection name
1977
+ * @param {Object[]} documents - Documents
1978
+ * @param {Object} options - refresh (undefined), retryOnConflict (0), timeout (undefined), userId (null)
1979
+ *
1980
+ * @returns {Promise.<{ items, errors }>
1981
+ */
1982
+ async mUpsert(index, collection, documents, { refresh, retryOnConflict = 0, timeout, userId = null, } = {}) {
1983
+ const alias = this._getAlias(index, collection);
1984
+ const esRequest = {
1985
+ operations: [],
1986
+ refresh,
1987
+ timeout,
1988
+ };
1989
+ const user = getKuid(userId);
1990
+ const now = Date.now();
1991
+ const kuzzleMeta = {
1992
+ doc: {
1993
+ _kuzzle_info: {
1994
+ updatedAt: now,
1995
+ updater: user,
1996
+ },
1997
+ },
1998
+ upsert: {
1999
+ _kuzzle_info: {
2000
+ author: user,
2001
+ createdAt: now,
2002
+ },
2003
+ },
2004
+ };
2005
+ const { rejected, extractedDocuments } = this._extractMDocuments(documents, kuzzleMeta, {
2006
+ prepareMUpsert: true,
2007
+ requireId: true,
2008
+ });
2009
+ /**
2010
+ * @warning Critical code section
2011
+ *
2012
+ * request can contain more than 10K elements
2013
+ */
2014
+ for (let i = 0; i < extractedDocuments.length; i++) {
2015
+ esRequest.operations.push({
2016
+ update: {
2017
+ _id: extractedDocuments[i]._id,
2018
+ _index: alias,
2019
+ _source: true,
2020
+ retry_on_conflict: retryOnConflict || this._config.defaults.onUpdateConflictRetries,
2021
+ },
2022
+ }, {
2023
+ doc: extractedDocuments[i]._source.changes,
2024
+ upsert: extractedDocuments[i]._source.default,
2025
+ });
2026
+ // _source: true
2027
+ // Makes ES return the updated document source in the response.
2028
+ // Required by the real-time notifier component
2029
+ }
2030
+ /* end critical code section */
2031
+ const response = await this._mExecute(esRequest, extractedDocuments, rejected);
2032
+ // with _source: true, ES returns the updated document in
2033
+ // response.result.get._source
2034
+ // => we replace response.result._source with it so that the notifier
2035
+ // module can seamlessly process all kind of m* response*
2036
+ response.items = response.items.map((item) => ({
2037
+ _id: item._id,
2038
+ _source: item.get._source,
2039
+ _version: item._version,
2040
+ created: item.result === "created", // Needed by the notifier
2041
+ status: item.status,
2042
+ }));
2043
+ return response;
2044
+ }
2045
+ /**
2046
+ * Replaces multiple documents at once.
2047
+ * Replacements are rejected if targeted documents do not exist
2048
+ * (like with the normal "replace" method)
2049
+ *
2050
+ * @param {String} index - Index name
2051
+ * @param {String} collection - Collection name
2052
+ * @param {Object[]} documents - Documents
2053
+ * @param {Object} options - timeout (undefined), refresh (undefined), userId (null)
2054
+ *
2055
+ * @returns {Promise.<Object>} { items, errors }
2056
+ */
2057
+ async mReplace(index, collection, documents, { refresh, timeout, userId = null, } = {}) {
2058
+ const alias = this._getAlias(index, collection), kuzzleMeta = {
2059
+ _kuzzle_info: {
2060
+ author: getKuid(userId),
2061
+ createdAt: Date.now(),
2062
+ updatedAt: null,
2063
+ updater: null,
2064
+ },
2065
+ }, { rejected, extractedDocuments, documentsToGet } = this._extractMDocuments(documents, kuzzleMeta, {
2066
+ prepareMGet: true,
2067
+ requireId: true,
2068
+ });
2069
+ if (documentsToGet.length < 1) {
2070
+ return { errors: rejected, items: [] };
2071
+ }
2072
+ const body = await this._client.mget({
2073
+ docs: documentsToGet,
2074
+ index: alias,
2075
+ });
2076
+ const existingDocuments = body.docs;
2077
+ const esRequest = {
2078
+ operations: [],
2079
+ refresh,
2080
+ timeout,
2081
+ };
2082
+ const toImport = [];
2083
+ /**
2084
+ * @warning Critical code section
2085
+ *
2086
+ * request can contain more than 10K elements
2087
+ */
2088
+ for (let i = 0; i < extractedDocuments.length; i++) {
2089
+ const document = extractedDocuments[i];
2090
+ // Documents are retrieved in the same order than we got them from user
2091
+ const doc = existingDocuments[i];
2092
+ if (!("error" in doc) && doc?.found) {
2093
+ esRequest.operations.push({
2094
+ index: {
2095
+ _id: document._id,
2096
+ _index: alias,
2097
+ },
2098
+ });
2099
+ esRequest.operations.push(document._source);
2100
+ toImport.push(document);
2101
+ }
2102
+ else {
2103
+ document._source._kuzzle_info = undefined;
2104
+ rejected.push({
2105
+ document: {
2106
+ _id: document._id,
2107
+ body: document._source,
2108
+ },
2109
+ reason: "document not found",
2110
+ status: 404,
2111
+ });
2112
+ }
2113
+ }
2114
+ /* end critical code section */
2115
+ return this._mExecute(esRequest, toImport, rejected);
2116
+ }
2117
+ /**
2118
+ * Deletes multiple documents with one request
2119
+ *
2120
+ * @param {String} index - Index name
2121
+ * @param {String} collection - Collection name
2122
+ * @param {Array.<String>} ids - Documents IDs
2123
+ * @param {Object} options - timeout (undefined), refresh (undefined)
2124
+ *
2125
+ * @returns {Promise.<{ documents, errors }>
2126
+ */
2127
+ async mDelete(index, collection, ids, { refresh, } = {}) {
2128
+ const query = { ids: { values: [] } };
2129
+ const validIds = [];
2130
+ const partialErrors = [];
2131
+ /**
2132
+ * @warning Critical code section
2133
+ *
2134
+ * request can contain more than 10K elements
2135
+ */
2136
+ for (let i = 0; i < ids.length; i++) {
2137
+ const _id = ids[i];
2138
+ if (typeof _id === "string") {
2139
+ validIds.push(_id);
2140
+ }
2141
+ else {
2142
+ partialErrors.push({
2143
+ _id,
2144
+ reason: "document _id must be a string",
2145
+ status: 400,
2146
+ });
2147
+ }
2148
+ }
2149
+ /* end critical code section */
2150
+ await this.refreshCollection(index, collection);
2151
+ const { items } = await this.mGet(index, collection, validIds);
2152
+ let idx = 0;
2153
+ /**
2154
+ * @warning Critical code section
2155
+ *
2156
+ * request can contain more than 10K elements
2157
+ */
2158
+ for (let i = 0; i < validIds.length; i++) {
2159
+ const validId = validIds[i];
2160
+ const item = items[idx];
2161
+ if (item && item._id === validId) {
2162
+ query.ids.values.push(validId);
2163
+ idx++;
2164
+ }
2165
+ else {
2166
+ partialErrors.push({
2167
+ _id: validId,
2168
+ reason: "document not found",
2169
+ status: 404,
2170
+ });
2171
+ }
2172
+ }
2173
+ /* end critical code section */
2174
+ // @todo duplicated query to get documents body, mGet here and search in
2175
+ // deleteByQuery
2176
+ const { documents } = await this.deleteByQuery(index, collection, query, {
2177
+ refresh,
2178
+ });
2179
+ return { documents, errors: partialErrors };
2180
+ }
2181
+ /**
2182
+ * Executes an ES request prepared by mcreate, mupdate, mreplace, mdelete or mwriteDocuments
2183
+ * Returns a standardized ES response object, containing the list of
2184
+ * successfully performed operations, and the rejected ones
2185
+ *
2186
+ * @param {Object} esRequest - Elasticsearch request
2187
+ * @param {Object[]} documents - Document sources (format: {_id, _source})
2188
+ * @param {Object[]} partialErrors - pre-rejected documents
2189
+ * @param {Object} options - limits (true)
2190
+ *
2191
+ * @returns {Promise.<Object[]>} results
2192
+ */
2193
+ async _mExecute(esRequest, documents, partialErrors = [], { limits = true, source = true } = {}) {
2194
+ assertWellFormedRefresh(esRequest);
2195
+ if (this._hasExceededLimit(limits, documents)) {
2196
+ return kerror.reject("services", "storage", "write_limit_exceeded");
2197
+ }
2198
+ let body = { items: [] };
2199
+ if (documents.length > 0) {
2200
+ try {
2201
+ body = await this._client.bulk(esRequest);
2202
+ }
2203
+ catch (error) {
2204
+ throw this._esWrapper.formatESError(error);
2205
+ }
2206
+ }
2207
+ const successes = [];
2208
+ /**
2209
+ * @warning Critical code section
2210
+ *
2211
+ * request can contain more than 10K elements
2212
+ */
2213
+ for (let i = 0; i < body.items.length; i++) {
2214
+ const item = body.items[i];
2215
+ const result = item[Object.keys(item)[0]];
2216
+ if (result.status >= 400) {
2217
+ if (result.status === 404) {
2218
+ partialErrors.push({
2219
+ document: {
2220
+ _id: documents[i]._id,
2221
+ body: documents[i]._source,
2222
+ },
2223
+ reason: "document not found",
2224
+ status: result.status,
2225
+ });
2226
+ }
2227
+ else {
2228
+ partialErrors.push({
2229
+ document: documents[i],
2230
+ reason: result.error.reason,
2231
+ status: result.status,
2232
+ });
2233
+ }
2234
+ }
2235
+ else {
2236
+ successes.push({
2237
+ _id: result._id,
2238
+ _source: source ? documents[i]._source : undefined,
2239
+ _version: result._version,
2240
+ created: result.result === "created",
2241
+ get: result.get,
2242
+ result: result.result,
2243
+ status: result.status, // used by mUpdate to get the full document body
2244
+ });
2245
+ }
2246
+ }
2247
+ /* end critical code section */
2248
+ return {
2249
+ errors: partialErrors, // @todo rename items to documents
2250
+ items: successes,
2251
+ };
2252
+ }
2253
+ /**
2254
+ * Extracts, injects metadata and validates documents contained
2255
+ * in a Request
2256
+ *
2257
+ * Used by mCreate, mUpdate, mUpsert, mReplace and mCreateOrReplace
2258
+ *
2259
+ * @param {Object[]} documents - Documents
2260
+ * @param {Object} metadata - Kuzzle metadata
2261
+ * @param {Object} options - prepareMGet (false), requireId (false)
2262
+ *
2263
+ * @returns {Object} { rejected, extractedDocuments, documentsToGet }
2264
+ */
2265
+ _extractMDocuments(documents, metadata, { prepareMGet = false, requireId = false, prepareMUpsert = false } = {}) {
2266
+ const rejected = [];
2267
+ const extractedDocuments = [];
2268
+ const documentsToGet = [];
2269
+ /**
2270
+ * @warning Critical code section
2271
+ *
2272
+ * request can contain more than 10K elements
2273
+ */
2274
+ for (let i = 0; i < documents.length; i++) {
2275
+ const document = documents[i];
2276
+ if (!(0, safeObject_1.isPlainObject)(document.body) && !prepareMUpsert) {
2277
+ rejected.push({
2278
+ document,
2279
+ reason: "document body must be an object",
2280
+ status: 400,
2281
+ });
2282
+ }
2283
+ else if (!(0, safeObject_1.isPlainObject)(document.changes) && prepareMUpsert) {
2284
+ rejected.push({
2285
+ document,
2286
+ reason: "document changes must be an object",
2287
+ status: 400,
2288
+ });
2289
+ }
2290
+ else if (prepareMUpsert &&
2291
+ document.default &&
2292
+ !(0, safeObject_1.isPlainObject)(document.default)) {
2293
+ rejected.push({
2294
+ document,
2295
+ reason: "document default must be an object",
2296
+ status: 400,
2297
+ });
2298
+ }
2299
+ else if (requireId && typeof document._id !== "string") {
2300
+ rejected.push({
2301
+ document,
2302
+ reason: "document _id must be a string",
2303
+ status: 400,
2304
+ });
2305
+ }
2306
+ else {
2307
+ this._processExtract(prepareMUpsert, prepareMGet, metadata, document, extractedDocuments, documentsToGet);
2308
+ }
2309
+ }
2310
+ /* end critical code section */
2311
+ return { documentsToGet, extractedDocuments, rejected };
2312
+ }
2313
+ _hasExceededLimit(limits, documents) {
2314
+ return (limits &&
2315
+ documents.length > global.kuzzle.config.limits.documentsWriteCount);
2316
+ }
2317
+ _processExtract(prepareMUpsert, prepareMGet, metadata, document, extractedDocuments, documentsToGet) {
2318
+ let extractedDocument;
2319
+ if (prepareMUpsert) {
2320
+ extractedDocument = {
2321
+ _source: {
2322
+ // Do not use destructuring, it's 10x slower
2323
+ changes: Object.assign({}, metadata.doc, document.changes),
2324
+ default: Object.assign({}, metadata.upsert, document.changes, document.default),
2325
+ },
2326
+ };
2327
+ }
2328
+ else {
2329
+ extractedDocument = {
2330
+ // Do not use destructuring, it's 10x slower
2331
+ _source: Object.assign({}, metadata, document.body),
2332
+ };
2333
+ }
2334
+ if (document._id) {
2335
+ extractedDocument._id = document._id;
2336
+ }
2337
+ extractedDocuments.push(extractedDocument);
2338
+ if (prepareMGet && typeof document._id === "string") {
2339
+ documentsToGet.push({
2340
+ _id: document._id,
2341
+ _source: false,
2342
+ });
2343
+ }
2344
+ }
2345
+ /**
2346
+ * Throws an error if the provided mapping is invalid
2347
+ *
2348
+ * @param {Object} mapping
2349
+ * @throws
2350
+ */
2351
+ _checkMappings(mapping, path = [], check = true) {
2352
+ const properties = Object.keys(mapping);
2353
+ const mappingProperties = path.length === 0
2354
+ ? ROOT_MAPPING_PROPERTIES
2355
+ : [...ROOT_MAPPING_PROPERTIES, ...CHILD_MAPPING_PROPERTIES];
2356
+ for (const property of properties) {
2357
+ if (check && !mappingProperties.includes(property)) {
2358
+ const currentPath = [...path, property].join(".");
2359
+ throw kerror.get("services", "storage", "invalid_mapping", currentPath, (0, didYouMean_1.default)(property, mappingProperties));
2360
+ }
2361
+ if (property === "properties") {
2362
+ // type definition level, we don't check
2363
+ this._checkMappings(mapping[property], [...path, "properties"], false);
2364
+ }
2365
+ else if (mapping[property]?.properties) {
2366
+ // root properties level, check for "properties", "dynamic" and "_meta"
2367
+ this._checkMappings(mapping[property], [...path, property], true);
2368
+ }
2369
+ }
2370
+ }
2371
+ /**
2372
+ * Given index + collection, returns the associated alias name.
2373
+ * Prefer this function to `_getIndice` and `_getAvailableIndice` whenever it is possible.
2374
+ *
2375
+ * @param {String} index
2376
+ * @param {String} collection
2377
+ *
2378
+ * @returns {String} Alias name (eg: '@&nepali.liia')
2379
+ */
2380
+ _getAlias(index, collection) {
2381
+ return `${ALIAS_PREFIX}${this._indexPrefix}${index}${NAME_SEPARATOR}${collection}`;
2382
+ }
2383
+ /**
2384
+ * Given an alias name, returns the associated index name.
2385
+ */
2386
+ async _checkIfAliasExists(aliasName) {
2387
+ return this._client.indices.existsAlias({
2388
+ name: aliasName,
2389
+ });
2390
+ }
2391
+ /**
2392
+ * Given index + collection, returns the associated indice name.
2393
+ * Use this function if ES does not accept aliases in the request. Otherwise use `_getAlias`.
2394
+ *
2395
+ * @param {String} index
2396
+ * @param {String} collection
2397
+ *
2398
+ * @returns {String} Indice name (eg: '&nepali.liia')
2399
+ * @throws If there is not exactly one indice associated
2400
+ */
2401
+ async _getIndice(index, collection) {
2402
+ const alias = `${ALIAS_PREFIX}${this._indexPrefix}${index}${NAME_SEPARATOR}${collection}`;
2403
+ const body = await this._client.cat.aliases({
2404
+ format: "json",
2405
+ name: alias,
2406
+ });
2407
+ if (body.length < 1) {
2408
+ throw kerror.get("services", "storage", "unknown_index_collection");
2409
+ }
2410
+ else if (body.length > 1) {
2411
+ throw kerror.get("services", "storage", "multiple_indice_alias", `"alias" starting with "${ALIAS_PREFIX}"`, '"indices"');
2412
+ }
2413
+ return body[0].index;
2414
+ }
2415
+ /**
2416
+ * Given an ES Request returns the settings of the corresponding indice.
2417
+ *
2418
+ * @param esRequest the ES Request with wanted settings.
2419
+ * @return {Promise<*>} the settings of the indice.
2420
+ * @private
2421
+ */
2422
+ async _getSettings(esRequest) {
2423
+ const response = await this._client.indices.getSettings(esRequest);
2424
+ const index = esRequest.index;
2425
+ return response[index].settings;
2426
+ }
2427
+ /**
2428
+ * Given index + collection, returns an available indice name.
2429
+ * Use this function when creating the associated indice. Otherwise use `_getAlias`.
2430
+ *
2431
+ * @param {String} index
2432
+ * @param {String} collection
2433
+ *
2434
+ * @returns {String} Available indice name (eg: '&nepali.liia2')
2435
+ */
2436
+ async _getAvailableIndice(index, collection) {
2437
+ let indice = this._getAlias(index, collection).substring(INDEX_PREFIX_POSITION_IN_ALIAS);
2438
+ if (!(await this._client.indices.exists({ index: indice }))) {
2439
+ return indice;
2440
+ }
2441
+ let notAvailable;
2442
+ let suffix;
2443
+ do {
2444
+ suffix = `.${this._getRandomNumber(100000)}`;
2445
+ const overflow = Buffer.from(indice + suffix).length - 255;
2446
+ if (overflow > 0) {
2447
+ const indiceBuffer = Buffer.from(indice);
2448
+ indice = indiceBuffer
2449
+ .subarray(0, indiceBuffer.length - overflow)
2450
+ .toString();
2451
+ }
2452
+ notAvailable = await this._client.indices.exists({
2453
+ index: indice + suffix,
2454
+ });
2455
+ } while (notAvailable);
2456
+ return indice + suffix;
2457
+ }
2458
+ /**
2459
+ * Given an indice, returns the associated alias name.
2460
+ *
2461
+ * @param {String} indice
2462
+ *
2463
+ * @returns {String} Alias name (eg: '@&nepali.liia')
2464
+ * @throws If there is not exactly one alias associated that is prefixed with @
2465
+ */
2466
+ async _getAliasFromIndice(indice) {
2467
+ const body = await this._client.indices.getAlias({ index: indice });
2468
+ const aliases = Object.keys(body[indice].aliases).filter((alias) => alias.startsWith(ALIAS_PREFIX));
2469
+ if (aliases.length < 1) {
2470
+ throw kerror.get("services", "storage", "unknown_index_collection");
2471
+ }
2472
+ return aliases;
2473
+ }
2474
+ /**
2475
+ * Check for each indice whether it has an alias or not.
2476
+ * When the latter is missing, create one based on the indice name.
2477
+ *
2478
+ * This check avoids a breaking change for those who were using Kuzzle before
2479
+ * alias attribution for each indice turned into a standard (appear in 2.14.0).
2480
+ */
2481
+ async generateMissingAliases() {
2482
+ try {
2483
+ const body = await this._client.cat.indices({ format: "json" });
2484
+ const indices = body.map(({ index: indice }) => indice);
2485
+ const aliases = await this.listAliases();
2486
+ const indicesWithoutAlias = indices.filter((indice) => indice[INDEX_PREFIX_POSITION_IN_INDICE] === this._indexPrefix &&
2487
+ !aliases.some((alias) => alias.indice === indice));
2488
+ const esRequest = { body: { actions: [] } };
2489
+ for (const indice of indicesWithoutAlias) {
2490
+ esRequest.body.actions.push({
2491
+ add: { alias: `${ALIAS_PREFIX}${indice}`, index: indice },
2492
+ });
2493
+ }
2494
+ if (esRequest.body.actions.length > 0) {
2495
+ await this._client.indices.updateAliases(esRequest);
2496
+ }
2497
+ }
2498
+ catch (error) {
2499
+ throw this._esWrapper.formatESError(error);
2500
+ }
2501
+ }
2502
+ /**
2503
+ * Throws if index or collection includes forbidden characters
2504
+ *
2505
+ * @param {String} index
2506
+ * @param {String} collection
2507
+ */
2508
+ _assertValidIndexAndCollection(index, collection = null) {
2509
+ if (!this.isIndexNameValid(index)) {
2510
+ throw kerror.get("services", "storage", "invalid_index_name", index);
2511
+ }
2512
+ if (collection !== null && !this.isCollectionNameValid(collection)) {
2513
+ throw kerror.get("services", "storage", "invalid_collection_name", collection);
2514
+ }
2515
+ }
2516
+ /**
2517
+ * Given an alias, extract the associated index.
2518
+ *
2519
+ * @param {String} alias
2520
+ *
2521
+ * @returns {String} Index name
2522
+ */
2523
+ _extractIndex(alias) {
2524
+ return alias.substr(INDEX_PREFIX_POSITION_IN_ALIAS + 1, alias.indexOf(NAME_SEPARATOR) - INDEX_PREFIX_POSITION_IN_ALIAS - 1);
2525
+ }
2526
+ /**
2527
+ * Given an alias, extract the associated collection.
2528
+ *
2529
+ * @param {String} alias
2530
+ *
2531
+ * @returns {String} Collection name
2532
+ */
2533
+ _extractCollection(alias) {
2534
+ const separatorPos = alias.indexOf(NAME_SEPARATOR);
2535
+ return alias.substr(separatorPos + 1, alias.length);
2536
+ }
2537
+ /**
2538
+ * Given aliases, extract indexes and collections.
2539
+ *
2540
+ * @param {Array.<String>} aliases
2541
+ * @param {Object.Boolean} includeHidden Only refers to `HIDDEN_COLLECTION` occurences. An empty index will still be listed. Default to `false`.
2542
+ *
2543
+ * @returns {Object.<String, String[]>} Indexes as key and an array of their collections as value
2544
+ */
2545
+ _extractSchema(aliases, { includeHidden = false } = {}) {
2546
+ const schema = {};
2547
+ for (const alias of aliases) {
2548
+ const [indexName, collectionName] = alias
2549
+ .slice(INDEX_PREFIX_POSITION_IN_ALIAS + 1)
2550
+ .split(NAME_SEPARATOR);
2551
+ if (alias[INDEX_PREFIX_POSITION_IN_ALIAS] === this._indexPrefix &&
2552
+ (collectionName !== HIDDEN_COLLECTION || includeHidden)) {
2553
+ if (!schema[indexName]) {
2554
+ schema[indexName] = [];
2555
+ }
2556
+ if (!schema[indexName].includes(collectionName)) {
2557
+ schema[indexName].push(collectionName);
2558
+ }
2559
+ }
2560
+ }
2561
+ return schema;
2562
+ }
2563
+ /**
2564
+ * Creates the hidden collection on the provided index if it does not already
2565
+ * exists
2566
+ *
2567
+ * @param {String} index Index name
2568
+ */
2569
+ async _createHiddenCollection(index) {
2570
+ const mutex = new mutex_1.Mutex(`hiddenCollection/${index}`);
2571
+ try {
2572
+ await mutex.lock();
2573
+ if (await this._hasHiddenCollection(index)) {
2574
+ return;
2575
+ }
2576
+ const esRequest = {
2577
+ aliases: {
2578
+ [this._getAlias(index, HIDDEN_COLLECTION)]: {},
2579
+ },
2580
+ index: await this._getAvailableIndice(index, HIDDEN_COLLECTION),
2581
+ settings: {
2582
+ number_of_replicas: this._config.defaultSettings.number_of_replicas,
2583
+ number_of_shards: this._config.defaultSettings.number_of_shards,
2584
+ },
2585
+ wait_for_active_shards: await this._getWaitForActiveShards(),
2586
+ };
2587
+ await this._client.indices.create(esRequest);
2588
+ }
2589
+ catch (e) {
2590
+ throw this._esWrapper.formatESError(e);
2591
+ }
2592
+ finally {
2593
+ await mutex.unlock();
2594
+ }
2595
+ }
2596
+ /**
2597
+ * We need to always wait for a minimal number of shards to be available
2598
+ * before answering to the client. This is to avoid Elasticsearch node
2599
+ * to return a 404 Not Found error when the client tries to index a
2600
+ * document in the index.
2601
+ * To find the best value for this setting, we need to take into account
2602
+ * the number of nodes in the cluster and the number of shards per index.
2603
+ */
2604
+ async _getWaitForActiveShards() {
2605
+ const body = await this._client.cat.nodes({ format: "json" });
2606
+ const numberOfNodes = body.length;
2607
+ if (numberOfNodes > 1) {
2608
+ return "all";
2609
+ }
2610
+ return 1;
2611
+ }
2612
+ /**
2613
+ * Scroll indice in elasticsearch and return all document that match the filter
2614
+ * /!\ throws a write_limit_exceed error: this method is intended to be used
2615
+ * by deleteByQuery and updateByQuery
2616
+ *
2617
+ * @param {Object} esRequest - Search request body
2618
+ *
2619
+ * @returns {Promise.<Array>} resolve to an array of documents
2620
+ */
2621
+ async _getAllDocumentsFromQuery(esRequest) {
2622
+ let { hits, _scroll_id } = await this._client.search(esRequest);
2623
+ const totalHitsValue = this._getHitsTotalValue(hits);
2624
+ if (totalHitsValue > global.kuzzle.config.limits.documentsWriteCount) {
2625
+ throw kerror.get("services", "storage", "write_limit_exceeded");
2626
+ }
2627
+ let documents = hits.hits.map((h) => ({
2628
+ _id: h._id,
2629
+ _source: h._source,
2630
+ body: {},
2631
+ }));
2632
+ while (totalHitsValue !== documents.length) {
2633
+ ({ hits, _scroll_id } = await this._client.scroll({
2634
+ scroll: esRequest.scroll,
2635
+ scroll_id: _scroll_id,
2636
+ }));
2637
+ documents = documents.concat(hits.hits.map((h) => ({
2638
+ _id: h._id,
2639
+ _source: h._source,
2640
+ body: {},
2641
+ })));
2642
+ }
2643
+ await this.clearScroll(_scroll_id);
2644
+ return documents;
2645
+ }
2646
+ /**
2647
+ * Clean and normalize the searchBody
2648
+ * Ensure only allowed parameters are passed to ES
2649
+ *
2650
+ * @param {Object} searchBody - ES search body (with query, aggregations, sort, etc)
2651
+ */
2652
+ _sanitizeSearchBody(searchBody) {
2653
+ // Only allow a whitelist of top level properties
2654
+ for (const key of Object.keys(searchBody)) {
2655
+ if (searchBody[key] !== undefined && !this.searchBodyKeys.includes(key)) {
2656
+ throw kerror.get("services", "storage", "invalid_search_query", key);
2657
+ }
2658
+ }
2659
+ // Ensure that the body does not include a script
2660
+ this._scriptCheck(searchBody);
2661
+ // Avoid empty queries that causes ES to respond with an error.
2662
+ // Empty queries are turned into match_all queries
2663
+ if (lodash_1.default.isEmpty(searchBody.query)) {
2664
+ searchBody.query = { match_all: {} };
2665
+ }
2666
+ return searchBody;
2667
+ }
2668
+ /**
2669
+ * Throw if a script is used in the query.
2670
+ *
2671
+ * Only Stored Scripts are accepted
2672
+ *
2673
+ * @param {Object} object
2674
+ */
2675
+ _scriptCheck(object) {
2676
+ for (const [key, value] of Object.entries(object)) {
2677
+ if (this.scriptKeys.includes(key)) {
2678
+ for (const scriptArg of Object.keys(value)) {
2679
+ if (!this.scriptAllowedArgs.includes(scriptArg)) {
2680
+ throw kerror.get("services", "storage", "invalid_query_keyword", `${key}.${scriptArg}`);
2681
+ }
2682
+ }
2683
+ }
2684
+ // Every object must be checked here, even the ones nested into an array
2685
+ else if (typeof value === "object" && value !== null) {
2686
+ this._scriptCheck(value);
2687
+ }
2688
+ }
2689
+ }
2690
+ /**
2691
+ * Checks if a collection name is valid
2692
+ * @param {string} name
2693
+ * @returns {Boolean}
2694
+ */
2695
+ isCollectionNameValid(name) {
2696
+ return _isObjectNameValid(name);
2697
+ }
2698
+ /**
2699
+ * Checks if a collection name is valid
2700
+ * @param {string} name
2701
+ * @returns {Boolean}
2702
+ */
2703
+ isIndexNameValid(name) {
2704
+ return _isObjectNameValid(name);
2705
+ }
2706
+ /**
2707
+ * Clears an allocated scroll
2708
+ * @param {[type]} id [description]
2709
+ * @returns {[type]} [description]
2710
+ */
2711
+ async clearScroll(id) {
2712
+ if (id) {
2713
+ (0, debug_1.default)("clearing scroll: %s", id);
2714
+ await this._client.clearScroll({ scroll_id: id });
2715
+ }
2716
+ }
2717
+ /**
2718
+ * Loads a configuration value from services.storageEngine and assert a valid
2719
+ * ms format.
2720
+ *
2721
+ * @param {String} key - relative path to the key in configuration
2722
+ *
2723
+ * @returns {Number} milliseconds
2724
+ */
2725
+ _loadMsConfig(key) {
2726
+ const configValue = lodash_1.default.get(this._config, key);
2727
+ (0, assert_1.default)(typeof configValue === "string", `services.storageEngine.${key} must be a string.`);
2728
+ const parsedValue = (0, ms_1.default)(configValue);
2729
+ (0, assert_1.default)(typeof parsedValue === "number", `Invalid parsed value from ms() for services.storageEngine.${key} ("${typeof parsedValue}").`);
2730
+ return parsedValue;
2731
+ }
2732
+ /**
2733
+ * Returns true if one of the mappings dynamic property changes value from
2734
+ * false to true
2735
+ */
2736
+ _dynamicChanges(previousMappings, newMappings) {
2737
+ const previousValues = findDynamic(previousMappings);
2738
+ for (const [path, previousValue] of Object.entries(previousValues)) {
2739
+ if (previousValue.toString() !== "false") {
2740
+ continue;
2741
+ }
2742
+ const newValue = lodash_1.default.get(newMappings, path);
2743
+ if (newValue && newValue.toString() !== "false") {
2744
+ return true;
2745
+ }
2746
+ }
2747
+ return false;
2748
+ }
2749
+ async waitForElasticsearch() {
2750
+ if (esState !== esStateEnum.NONE) {
2751
+ while (esState !== esStateEnum.OK) {
2752
+ await bluebird_1.default.delay(1000);
2753
+ }
2754
+ return;
2755
+ }
2756
+ esState = esStateEnum.AWAITING;
2757
+ global.kuzzle.log.info("[ℹ] Trying to connect to Elasticsearch...");
2758
+ while (esState !== esStateEnum.OK) {
2759
+ try {
2760
+ // Wait for at least 1 shard to be initialized
2761
+ const health = await this._client.cluster.health({
2762
+ wait_for_no_initializing_shards: true,
2763
+ });
2764
+ if (health.number_of_pending_tasks === 0) {
2765
+ global.kuzzle.log.info("[✔] Elasticsearch is ready");
2766
+ esState = esStateEnum.OK;
2767
+ }
2768
+ else {
2769
+ global.kuzzle.log.info(`[ℹ] Still waiting for Elasticsearch: ${health.number_of_pending_tasks} cluster tasks remaining`);
2770
+ await bluebird_1.default.delay(1000);
2771
+ }
2772
+ }
2773
+ catch (e) {
2774
+ await bluebird_1.default.delay(1000);
2775
+ }
2776
+ }
2777
+ }
2778
+ /**
2779
+ * Checks if the dynamic properties are correct
2780
+ */
2781
+ _checkDynamicProperty(mappings) {
2782
+ const dynamicProperties = findDynamic(mappings);
2783
+ for (const [path, value] of Object.entries(dynamicProperties)) {
2784
+ // Prevent common mistake
2785
+ if (typeof value === "boolean") {
2786
+ lodash_1.default.set(mappings, path, value.toString());
2787
+ }
2788
+ else if (typeof value !== "string") {
2789
+ throw kerror.get("services", "storage", "invalid_mapping", path, "Dynamic property value should be a string.");
2790
+ }
2791
+ if (!DYNAMIC_PROPERTY_VALUES.includes(value.toString())) {
2792
+ throw kerror.get("services", "storage", "invalid_mapping", path, `Incorrect dynamic property value (${value}). Should be one of "${DYNAMIC_PROPERTY_VALUES.join('", "')}"`);
2793
+ }
2794
+ }
2795
+ }
2796
+ _setLastActionToKuzzleMeta(esRequest, alias, kuzzleMeta) {
2797
+ /**
2798
+ * @warning Critical code section
2799
+ *
2800
+ * bulk body can contain more than 10K elements
2801
+ */
2802
+ let lastAction = "";
2803
+ const actionNames = ["index", "create", "update", "delete"];
2804
+ for (let i = 0; i < esRequest.operations.length; i++) {
2805
+ const item = esRequest.operations[i];
2806
+ const action = Object.keys(item)[0];
2807
+ if (actionNames.indexOf(action) !== -1) {
2808
+ lastAction = action;
2809
+ item[action]._index = alias;
2810
+ if (item[action]?._type) {
2811
+ item[action]._type = undefined;
2812
+ }
2813
+ }
2814
+ else if (lastAction === "index" || lastAction === "create") {
2815
+ item._kuzzle_info = kuzzleMeta.created;
2816
+ }
2817
+ else if (lastAction === "update") {
2818
+ this._setLastActionToKuzzleMetaUpdate(item, kuzzleMeta);
2819
+ }
2820
+ }
2821
+ /* end critical code section */
2822
+ }
2823
+ _setLastActionToKuzzleMetaUpdate(item, kuzzleMeta) {
2824
+ for (const prop of ["doc", "upsert"]) {
2825
+ if ((0, safeObject_1.isPlainObject)(item[prop])) {
2826
+ item[prop]._kuzzle_info = kuzzleMeta.updated;
2827
+ }
2828
+ }
2829
+ }
2830
+ _getHitsTotalValue(hits) {
2831
+ if (typeof hits.total === "number") {
2832
+ return hits.total;
2833
+ }
2834
+ return hits.total.value;
2835
+ }
2836
+ _getRandomNumber(number) {
2837
+ return (0, name_generator_1.randomNumber)(number);
2838
+ }
2839
+ }
2840
+ exports.ES8 = ES8;
2841
+ /**
2842
+ * Finds paths and values of mappings dynamic properties
2843
+ *
2844
+ * @example
2845
+ *
2846
+ * findDynamic(mappings);
2847
+ * {
2848
+ * "properties.metadata.dynamic": "true",
2849
+ * "properties.user.properties.address.dynamic": "strict"
2850
+ * }
2851
+ */
2852
+ function findDynamic(mappings, path = [], results = {}) {
2853
+ if (mappings.dynamic !== undefined) {
2854
+ results[path.concat("dynamic").join(".")] = mappings.dynamic;
2855
+ }
2856
+ for (const [key, value] of Object.entries(mappings)) {
2857
+ if ((0, safeObject_1.isPlainObject)(value)) {
2858
+ findDynamic(value, path.concat(key), results);
2859
+ }
2860
+ }
2861
+ return results;
2862
+ }
2863
+ /**
2864
+ * Forbids the use of the _routing ES option
2865
+ *
2866
+ * @param {Object} esRequest
2867
+ * @throws
2868
+ */
2869
+ function assertNoRouting(esRequest) {
2870
+ if (esRequest._routing) {
2871
+ throw kerror.get("services", "storage", "no_routing");
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Checks if the optional "refresh" argument is well-formed
2876
+ *
2877
+ * @param {Object} esRequest
2878
+ * @throws
2879
+ */
2880
+ function assertWellFormedRefresh(esRequest) {
2881
+ if (!["wait_for", "false", false, undefined].includes(esRequest.refresh)) {
2882
+ throw kerror.get("services", "storage", "invalid_argument", "refresh", '"wait_for", false');
2883
+ }
2884
+ }
2885
+ function getKuid(userId) {
2886
+ if (!userId) {
2887
+ return null;
2888
+ }
2889
+ return String(userId);
2890
+ }
2891
+ /**
2892
+ * Checks if an index or collection name is valid
2893
+ *
2894
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/7.4/indices-create-index.html
2895
+ *
2896
+ * Beware of the length check: ES allows indice names up to 255 bytes, but since
2897
+ * in Kuzzle we emulate collections as indices, we have to make sure
2898
+ * that the privacy prefix, the index name, the separator and the collection
2899
+ * name ALL fit within the 255-bytes limit of Elasticsearch. The simplest way
2900
+ * is to limit index and collection names to 126 bytes and document that
2901
+ * limitation (prefix(1) + index(1..126) + sep(1) + collection(1..126) = 4..254)
2902
+ *
2903
+ * @param {string} name
2904
+ * @returns {Boolean}
2905
+ */
2906
+ function _isObjectNameValid(name) {
2907
+ if (typeof name !== "string" || name.length === 0) {
2908
+ return false;
2909
+ }
2910
+ if (name.toLowerCase() !== name) {
2911
+ return false;
2912
+ }
2913
+ if (Buffer.from(name).length > 126) {
2914
+ return false;
2915
+ }
2916
+ if (name === "_all") {
2917
+ return false;
2918
+ }
2919
+ let valid = true;
2920
+ for (let i = 0; valid && i < FORBIDDEN_CHARS.length; i++) {
2921
+ valid = !name.includes(FORBIDDEN_CHARS[i]);
2922
+ }
2923
+ return valid;
2924
+ }
2925
+ //# sourceMappingURL=elasticsearch.js.map