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.
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/lib/api/controllers/authController.d.ts +3 -2
- package/lib/api/funnel.js +2 -1
- package/lib/config/default.config.js +1 -0
- package/lib/core/backend/backendStorage.d.ts +3 -5
- package/lib/core/backend/backendStorage.js +8 -10
- package/lib/core/plugin/pluginContext.d.ts +2 -3
- package/lib/core/plugin/pluginContext.js +6 -4
- package/lib/core/security/tokenRepository.d.ts +1 -1
- package/lib/core/security/tokenRepository.js +1 -1
- package/lib/core/shared/ObjectRepository.d.ts +1 -1
- package/lib/core/storage/clientAdapter.js +6 -4
- package/lib/core/storage/storageEngine.js +4 -5
- package/lib/kerror/index.js +1 -1
- package/lib/kuzzle/event/KuzzleEventEmitter.d.ts +70 -0
- package/lib/kuzzle/event/KuzzleEventEmitter.js +328 -0
- package/lib/kuzzle/index.d.ts +3 -0
- package/lib/kuzzle/index.js +7 -4
- package/lib/kuzzle/kuzzle.d.ts +32 -19
- package/lib/kuzzle/kuzzle.js +31 -31
- package/lib/service/storage/{elasticsearch.d.ts → 7/elasticsearch.d.ts} +40 -22
- package/lib/service/storage/{elasticsearch.js → 7/elasticsearch.js} +24 -43
- package/lib/service/storage/{esWrapper.js → 7/esWrapper.js} +6 -4
- package/lib/service/storage/8/elasticsearch.d.ts +972 -0
- package/lib/service/storage/8/elasticsearch.js +2925 -0
- package/lib/service/storage/8/esWrapper.js +303 -0
- package/lib/service/storage/Elasticsearch.d.ts +9 -0
- package/lib/service/storage/Elasticsearch.js +48 -0
- package/lib/service/storage/commons/queryTranslator.d.ts +5 -0
- package/lib/service/storage/commons/queryTranslator.js +189 -0
- package/lib/types/EventHandler.d.ts +29 -1
- package/lib/types/config/KuzzleConfiguration.d.ts +2 -1
- package/lib/types/config/storageEngine/StorageEngineElasticsearchConfiguration.d.ts +6 -2
- package/lib/types/storage/{Elasticsearch.d.ts → 7/Elasticsearch.d.ts} +1 -1
- package/lib/types/storage/8/Elasticsearch.d.ts +59 -0
- package/lib/types/storage/8/Elasticsearch.js +3 -0
- package/package.json +7 -4
- package/lib/kuzzle/event/kuzzleEventEmitter.js +0 -405
- package/lib/service/storage/queryTranslator.js +0 -219
- /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
|