kuzzle 2.60.0-beta.4 → 2.60.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/lib/api/controllers/adminController.d.ts +7 -1
  2. package/dist/lib/api/controllers/adminController.js +9 -2
  3. package/dist/lib/api/funnel.js +12 -1
  4. package/dist/lib/api/httpRoutes.js +52 -20
  5. package/dist/lib/api/request/kuzzleRequest.d.ts +7 -2
  6. package/dist/lib/api/request/kuzzleRequest.js +20 -10
  7. package/dist/lib/api/request/requestContext.d.ts +1 -1
  8. package/dist/lib/api/request/requestResponse.js +2 -1
  9. package/dist/lib/config/index.js +60 -3
  10. package/dist/lib/core/backend/backend.d.ts +3 -2
  11. package/dist/lib/core/backend/internalLogger.d.ts +6 -0
  12. package/dist/lib/core/backend/internalLogger.js +6 -0
  13. package/dist/lib/core/network/router.d.ts +18 -0
  14. package/dist/lib/core/network/router.js +44 -10
  15. package/dist/lib/core/security/authTokenSettings.d.ts +21 -0
  16. package/dist/lib/core/security/authTokenSettings.js +61 -0
  17. package/dist/lib/core/security/tokenRepository.js +8 -14
  18. package/dist/lib/core/shared/KoncordeWrapper.d.ts +9 -0
  19. package/dist/lib/core/shared/KoncordeWrapper.js +8 -8
  20. package/dist/lib/core/storage/clientAdapter.js +7 -2
  21. package/dist/lib/kuzzle/Logger.d.ts +4 -0
  22. package/dist/lib/kuzzle/Logger.js +9 -2
  23. package/dist/lib/kuzzle/internalIndexHandler.js +4 -5
  24. package/dist/lib/kuzzle/kuzzle.d.ts +17 -0
  25. package/dist/lib/kuzzle/kuzzle.js +62 -0
  26. package/dist/lib/kuzzle/vault.d.ts +13 -0
  27. package/dist/lib/kuzzle/vault.js +25 -7
  28. package/dist/lib/service/storage/7/elasticsearch.d.ts +12 -0
  29. package/dist/lib/service/storage/7/elasticsearch.js +37 -15
  30. package/dist/lib/service/storage/8/elasticsearch.d.ts +12 -0
  31. package/dist/lib/service/storage/8/elasticsearch.js +37 -15
  32. package/dist/lib/types/config/KuzzleConfiguration.d.ts +7 -0
  33. package/dist/lib/types/config/SecurityConfiguration.d.ts +4 -3
  34. package/dist/lib/types/controllers/ControllerDefinition.d.ts +4 -4
  35. package/dist/lib/util/mutex.d.ts +3 -3
  36. package/dist/lib/util/mutex.js +1 -1
  37. package/dist/package.json +1 -1
  38. package/package.json +1 -1
@@ -35,14 +35,14 @@ class NormalizedFilterV3 {
35
35
  }
36
36
  }
37
37
  exports.NormalizedFilterV3 = NormalizedFilterV3;
38
- /*
39
- @deprecated
40
-
41
- Exposes Koncorde v4 through an interface emulating Koncorde v3, to prevent
42
- breaking changes in Kuzzle.
43
-
44
- This class should either be removed entirely in the next major version, or
45
- perhaps proposed in a compatibility mode to help clients migrate smoothly.
38
+ /**
39
+ * Exposes Koncorde v4 through an interface emulating Koncorde v3, to prevent
40
+ * breaking changes in Kuzzle.
41
+ *
42
+ * This class should either be removed entirely in the next major version, or
43
+ * perhaps proposed in a compatibility mode to help clients migrate smoothly.
44
+ *
45
+ * @deprecated Use the `koncorde` package (v4) directly.
46
46
  */
47
47
  class Koncorde {
48
48
  constructor(options = {}) {
@@ -96,8 +96,13 @@ class ClientAdapter {
96
96
  this.registerLockEvents();
97
97
  // The cache and the locks were first loaded before this node joined the
98
98
  // cluster: an index, a collection or a lock a peer added in between was
99
- // broadcast before this node could hear it. Reading them again once the
100
- // cluster is joined closes that gap.
99
+ // broadcast before this node could hear it. `Kuzzle.start()` asks for this
100
+ // reload right after the cluster is joined, before the plugins' `init()`
101
+ // runs (#3044).
102
+ global.kuzzle.onAsk(`core:storage:${this.scope}:cache:reload`, () => this.reloadCache());
103
+ // Read again once the node goes live: a peer that joined after this node
104
+ // may have created indexes or collections before its own join (e.g. its
105
+ // internal index), and that broadcast reached no one.
101
106
  global.kuzzle.onPipe("kuzzle:state:live", () => this.reloadCache());
102
107
  // Global store events registration
103
108
  /**
@@ -28,5 +28,9 @@ export declare class Logger extends KuzzleLogger {
28
28
  verbose(message: string): void;
29
29
  getLevel(): string;
30
30
  setLevel(level: string): void;
31
+ /**
32
+ * Creates a child logger with the given namespace, appended to the parent's
33
+ * one. Its level follows the parent's until it is set on the child.
34
+ */
31
35
  child(namespace: string): Logger;
32
36
  }
@@ -128,9 +128,16 @@ class Logger extends kuzzle_logger_1.KuzzleLogger {
128
128
  setLevel(level) {
129
129
  this.level = level;
130
130
  }
131
+ /**
132
+ * Creates a child logger with the given namespace, appended to the parent's
133
+ * one. Its level follows the parent's until it is set on the child.
134
+ */
131
135
  child(namespace) {
132
- // TODO, this is fishy we will probably need to properly remove the "as Logger"
133
- return super.child(namespace);
136
+ // kuzzle-logger builds the child as a plain KuzzleLogger (no getLevel,
137
+ // setLevel, silly, verbose): give it this class's prototype so it is the
138
+ // Logger it is declared to be. Its state is all KuzzleLogger's, and the
139
+ // deprecation flags start unset, as on a new Logger.
140
+ return Object.setPrototypeOf(super.child(namespace), Logger.prototype);
134
141
  }
135
142
  }
136
143
  exports.Logger = Logger;
@@ -57,6 +57,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
57
57
  };
58
58
  const node_crypto_1 = __importDefault(require("node:crypto"));
59
59
  const bluebird_1 = __importDefault(require("bluebird"));
60
+ const authTokenSettings_1 = require("../core/security/authTokenSettings");
60
61
  const store_1 = require("../core/shared/store");
61
62
  const storeScopeEnum_1 = require("../core/storage/storeScopeEnum");
62
63
  const specification_1 = require("../core/validation/specification");
@@ -191,11 +192,9 @@ class InternalIndexHandler extends store_1.Store {
191
192
  await bluebird_1.default.all(promises);
192
193
  }
193
194
  async _initSecret() {
194
- // NOSONAR: `jwt` is the deprecated spelling of `authToken`, read second so
195
- // an existing configuration keeps working. Dropping it is a breaking change
196
- // for anyone who has not migrated, not a conversion's to make.
197
- const { authToken, jwt } = global.kuzzle.config.security; // NOSONAR
198
- const configSeed = authToken?.secret ?? jwt?.secret; // NOSONAR
195
+ // `jwt` is the deprecated spelling of `authToken`, read second so an
196
+ // existing configuration keeps working (see authTokenSetting).
197
+ const configSeed = (0, authTokenSettings_1.authTokenSetting)("secret");
199
198
  let storedSeed = await this.exists("config", this._JWT_SECRET_ID);
200
199
  if (!configSeed) {
201
200
  if (!storedSeed) {
@@ -107,6 +107,23 @@ declare class Kuzzle extends KuzzleEventEmitter {
107
107
  * it hands the application wrapper the implementation reads.
108
108
  */
109
109
  start(application: unknown, options?: StartOptions): Promise<void>;
110
+ /**
111
+ * A node asked to stop while it starts must not go on taking cluster-wide
112
+ * locks or running the application's installations: if it is killed before
113
+ * its shutdown releases them, the other nodes wait for their TTL (#3045).
114
+ */
115
+ private stopIfShuttingDown;
116
+ /**
117
+ * The shutdown began during the startup, which stopped (or failed because
118
+ * of it): no "Cannot start" error and no second shutdown, the one running
119
+ * keeps its exit code and ends the process.
120
+ *
121
+ * The returned promise never settles, as `withLock` once the process is
122
+ * exiting: rejecting would send the caller down its failed-start path
123
+ * (often a `process.exit(1)`, which would cut the shutdown short), and
124
+ * resolving would tell it that Kuzzle is running.
125
+ */
126
+ private leaveStartupToShutdown;
110
127
  /**
111
128
  * Generates the node ID.
112
129
  *
@@ -99,6 +99,17 @@ let _kuzzle = null;
99
99
  * not keep a failed node alive, which a bare `process.exit(1)` never did
100
100
  */
101
101
  const FAILURE_SHUTDOWN_TIMEOUT = 30000;
102
+ /**
103
+ * Stops the startup sequence once a shutdown has begun (#3045): `start()`
104
+ * catches it and leaves the shutdown to end the process. Internal: never
105
+ * handed to the caller of `start()`.
106
+ */
107
+ class StartupInterruptedError extends Error {
108
+ constructor() {
109
+ super("Kuzzle is shutting down");
110
+ this.name = "StartupInterruptedError";
111
+ }
112
+ }
102
113
  Reflect.defineProperty(global, "kuzzle", {
103
114
  configurable: true,
104
115
  enumerable: false,
@@ -175,9 +186,11 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
175
186
  await new cacheEngine_1.default().init();
176
187
  await new storageEngine_1.default().init();
177
188
  await new realtime_1.default().init();
189
+ this.stopIfShuttingDown();
178
190
  await this.internalIndex.init();
179
191
  await new security_1.default().init();
180
192
  // This will init the cluster module if enabled
193
+ this.stopIfShuttingDown();
181
194
  await this.initKuzzleNode();
182
195
  this.vault = vault_1.default.load(options?.vaultKey, options?.secretsFile, this.config.vault?.newAlgorithm);
183
196
  this.validation.init();
@@ -189,21 +202,25 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
189
202
  // before opening connections to external users
190
203
  await this.entryPoint.init();
191
204
  await this.debugger.init();
205
+ this.stopIfShuttingDown();
192
206
  this.pluginsManager.application = application;
193
207
  const pluginImports = await this.pluginsManager.init(options?.plugins);
194
208
  const imports = lodash_1.default.merge({}, pluginImports, options?.import);
195
209
  // Authentification plugins must be loaded before users import to avoid
196
210
  // credentials related error which would prevent Kuzzle from starting
211
+ this.stopIfShuttingDown();
197
212
  await this.loadInitialState(imports, options?.support);
198
213
  await this.ask("core:security:verify");
199
214
  this.router.init();
200
215
  this.log.info("[✔] Core components loaded");
216
+ this.stopIfShuttingDown();
201
217
  await this.install(options?.installations);
202
218
  // `application` is the very wrapper handed to the plugins manager six
203
219
  // lines up, so reading the name back off the manager was a round trip
204
220
  // through a getter that can answer `undefined`.
205
221
  this.log.info(`[✔] Start "${application.name}" application`);
206
222
  this.openApiManager = new openapi_1.OpenApiManager(application.openApi, this.config.http.routes, this.pluginsManager.routes);
223
+ this.stopIfShuttingDown();
207
224
  // @deprecated
208
225
  await this.pipe("kuzzle:start");
209
226
  await this.pipe("kuzzle:state:live");
@@ -215,6 +232,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
215
232
  this._state = kuzzleStateEnum_1.default.RUNNING;
216
233
  }
217
234
  catch (error) {
235
+ if (this._state === kuzzleStateEnum_1.default.SHUTTING_DOWN) {
236
+ return this.leaveStartupToShutdown(error);
237
+ }
218
238
  this.log.error(`[X] Cannot start Kuzzle ${this.version}: ${error instanceof Error ? error.message : (0, node_util_1.inspect)(error)}`);
219
239
  // A failed start cannot be retried (a second Kuzzle instance cannot be
220
240
  // built), and what it did start keeps the process alive: in production,
@@ -229,6 +249,36 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
229
249
  throw error;
230
250
  }
231
251
  }
252
+ /**
253
+ * A node asked to stop while it starts must not go on taking cluster-wide
254
+ * locks or running the application's installations: if it is killed before
255
+ * its shutdown releases them, the other nodes wait for their TTL (#3045).
256
+ */
257
+ stopIfShuttingDown() {
258
+ if (this._state === kuzzleStateEnum_1.default.SHUTTING_DOWN) {
259
+ throw new StartupInterruptedError();
260
+ }
261
+ }
262
+ /**
263
+ * The shutdown began during the startup, which stopped (or failed because
264
+ * of it): no "Cannot start" error and no second shutdown, the one running
265
+ * keeps its exit code and ends the process.
266
+ *
267
+ * The returned promise never settles, as `withLock` once the process is
268
+ * exiting: rejecting would send the caller down its failed-start path
269
+ * (often a `process.exit(1)`, which would cut the shutdown short), and
270
+ * resolving would tell it that Kuzzle is running.
271
+ */
272
+ leaveStartupToShutdown(error) {
273
+ let message = "[ℹ] Startup stopped: Kuzzle is shutting down";
274
+ // A step that failed because of the shutdown, rather than stopped by it
275
+ if (!(error instanceof StartupInterruptedError)) {
276
+ const reason = error instanceof Error ? error.message : (0, node_util_1.inspect)(error);
277
+ message += ` (${reason})`;
278
+ }
279
+ this.log.info(message);
280
+ return new Promise(() => undefined);
281
+ }
232
282
  /**
233
283
  * Generates the node ID.
234
284
  *
@@ -238,6 +288,13 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
238
288
  if (this.config.cluster.enabled) {
239
289
  await new cluster_1.default().init();
240
290
  this.log.info("[✔] Cluster initialized");
291
+ // The storage caches were loaded before the join: what a peer created
292
+ // in between was broadcast before this node could hear it. Read them
293
+ // again before the plugins' `init()` relies on them (#3044).
294
+ await Promise.all([
295
+ this.ask("core:storage:public:cache:reload"),
296
+ this.ask("core:storage:private:cache:reload"),
297
+ ]);
241
298
  }
242
299
  else {
243
300
  this.log.info("[X] Cluster disabled: single node mode.");
@@ -317,6 +374,9 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
317
374
  // one must not start before they are recorded
318
375
  await (0, distributedLock_1.withLock)("backend:installations", async () => {
319
376
  for (const installation of installations) {
377
+ // The lock may come long after it was asked for, once the node
378
+ // holding it is done: by then, this one may be shutting down
379
+ this.stopIfShuttingDown();
320
380
  const isAlreadyInstalled = await this.ask("core:storage:private:document:exist", "kuzzle", "installations", installation.id);
321
381
  if (!isAlreadyInstalled) {
322
382
  try {
@@ -508,6 +568,8 @@ class Kuzzle extends KuzzleEventEmitter_1.default {
508
568
  // cannot serialise, and the payload is built right above.
509
569
  const importPayloadHash = (0, crypto_1.sha256)((0, json_stable_stringify_1.default)(importPayload) ?? "");
510
570
  const importType = async () => {
571
+ // Same as for the installations: the lock may come after a SIGTERM
572
+ this.stopIfShuttingDown();
511
573
  // Read the bookkeeping *after* the lock, never before: a read taken
512
574
  // before acquiring it is not protected by it, and the holder that
513
575
  // acted on a stale "not initialized" used to create a document the
@@ -1,6 +1,19 @@
1
1
  import { Vault } from "kuzzle-vault";
2
+ /**
3
+ * The default secrets file, `config/secrets.enc.json` at the project root.
4
+ *
5
+ * Installed as an npm package (KaaF), Kuzzle lives under `node_modules/kuzzle`
6
+ * and the project root is above it: from `node_modules/kuzzle/dist/lib/kuzzle`
7
+ * since the build outputs to `dist/` (2.50.0), from
8
+ * `node_modules/kuzzle/lib/kuzzle` before. Anywhere else (a Kuzzle checkout),
9
+ * the file is the checkout's own `config/secrets.enc.json`.
10
+ *
11
+ * @param dirname - the directory of this module
12
+ */
13
+ declare function defaultSecretsFile(dirname: string): string;
2
14
  declare function load(vaultKey?: string, secretsFile?: string, useNewAlgorithm?: boolean): Vault;
3
15
  declare const _default: {
16
+ defaultSecretsFile: typeof defaultSecretsFile;
4
17
  load: typeof load;
5
18
  };
6
19
  export = _default;
@@ -35,15 +35,33 @@ const isEmpty_1 = __importDefault(require("lodash/isEmpty"));
35
35
  // strict while being false for every read before that first assignment
36
36
  // (TD-57, #2760).
37
37
  let ENV_VAULT_KEY;
38
+ /**
39
+ * The default secrets file, `config/secrets.enc.json` at the project root.
40
+ *
41
+ * Installed as an npm package (KaaF), Kuzzle lives under `node_modules/kuzzle`
42
+ * and the project root is above it: from `node_modules/kuzzle/dist/lib/kuzzle`
43
+ * since the build outputs to `dist/` (2.50.0), from
44
+ * `node_modules/kuzzle/lib/kuzzle` before. Anywhere else (a Kuzzle checkout),
45
+ * the file is the checkout's own `config/secrets.enc.json`.
46
+ *
47
+ * @param dirname - the directory of this module
48
+ */
49
+ function defaultSecretsFile(dirname) {
50
+ const dir = dirname.split(node_path_1.default.sep).join("/");
51
+ for (const installed of [
52
+ "/node_modules/kuzzle/dist/lib/kuzzle",
53
+ "/node_modules/kuzzle/lib/kuzzle",
54
+ ]) {
55
+ if (dir.endsWith(installed)) {
56
+ return node_path_1.default.resolve(dirname.slice(0, dirname.length - installed.length + 1), "config/secrets.enc.json");
57
+ }
58
+ }
59
+ return node_path_1.default.resolve(dirname, "../../config/secrets.enc.json");
60
+ }
38
61
  function load(vaultKey, secretsFile, useNewAlgorithm = false) {
39
- // Using KaaF kuzzle is an npm package and is located under node_modules folder
40
- // We need to get back to root folder of the project to get the secret file
41
- const defaultEncryptedSecretsFile = __dirname.endsWith("/node_modules/kuzzle/lib/kuzzle")
42
- ? node_path_1.default.resolve(`${__dirname}/../../../../config/secrets.enc.json`)
43
- : node_path_1.default.resolve(`${__dirname}/../../config/secrets.enc.json`);
44
62
  const encryptedSecretsFile = secretsFile ||
45
63
  process.env.KUZZLE_SECRETS_FILE ||
46
- defaultEncryptedSecretsFile;
64
+ defaultSecretsFile(__dirname);
47
65
  let key = vaultKey;
48
66
  if ((0, isEmpty_1.default)(vaultKey) &&
49
67
  (!(0, isEmpty_1.default)(process.env.KUZZLE_VAULT_KEY) || !(0, isEmpty_1.default)(ENV_VAULT_KEY))) {
@@ -71,5 +89,5 @@ function load(vaultKey, secretsFile, useNewAlgorithm = false) {
71
89
  }
72
90
  return vault;
73
91
  }
74
- module.exports = { load };
92
+ module.exports = { defaultSecretsFile, load };
75
93
  //# sourceMappingURL=vault.js.map
@@ -452,6 +452,12 @@ export declare class ES7 {
452
452
  mappings?: TypeMapping;
453
453
  settings?: JSONObject;
454
454
  }): Promise<null>;
455
+ /**
456
+ * Applies the mappings and settings of a `createCollection` call to a
457
+ * collection that already exists because another request created it,
458
+ * maybe so recently that its primary shards are still initializing.
459
+ */
460
+ private _updateConcurrentlyCreatedCollection;
455
461
  /**
456
462
  * Waits until the primary shards of a collection are active, for at most
457
463
  * `timeout`, and rejects if they are not by then.
@@ -462,6 +468,12 @@ export declare class ES7 {
462
468
  * collection waits for its shards (`wait_for_active_shards`); a node that
463
469
  * finds it already there calls this instead. It answers at once when the
464
470
  * shards are already active.
471
+ *
472
+ * A `yellow` status alone does not wait: Elasticsearch reports a new index
473
+ * whose primaries are still initializing as `yellow` (#3074). Asking for as
474
+ * many active shard copies as the index has primaries does, and, unlike
475
+ * `wait_for_no_initializing_shards`, does not also wait for replicas that
476
+ * are recovering, which a collection with data may take longer to do.
465
477
  */
466
478
  waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
467
479
  /**
@@ -1149,23 +1149,23 @@ class ES7 {
1149
1149
  this._checkDynamicProperty(mappings);
1150
1150
  const exists = await this.hasCollection(index, collection);
1151
1151
  if (exists) {
1152
- // Another node created it, maybe so recently that its primary shards
1153
- // are still initializing
1154
- await this.waitForCollectionShards(index, collection);
1155
- return this.updateCollection(index, collection, { mappings, settings });
1152
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1156
1153
  }
1157
1154
  this._checkMappings(mappings);
1155
+ // Copies, not in-place changes: the race branch below must hand the
1156
+ // caller's own mappings and settings to `updateCollection`
1158
1157
  requestBody.mappings = {
1159
1158
  _meta: mappings._meta || this._config.commonMapping._meta,
1160
1159
  dynamic: mappings.dynamic || this._config.commonMapping.dynamic,
1161
- properties: lodash_1.default.merge(mappings.properties, this._config.commonMapping.properties),
1160
+ properties: lodash_1.default.merge({}, mappings.properties, this._config.commonMapping.properties),
1161
+ };
1162
+ requestBody.settings = {
1163
+ ...settings,
1164
+ number_of_replicas: settings.number_of_replicas ||
1165
+ this._config.defaultSettings.number_of_replicas,
1166
+ number_of_shards: settings.number_of_shards ||
1167
+ this._config.defaultSettings.number_of_shards,
1162
1168
  };
1163
- settings.number_of_replicas =
1164
- settings.number_of_replicas ||
1165
- this._config.defaultSettings.number_of_replicas;
1166
- settings.number_of_shards =
1167
- settings.number_of_shards ||
1168
- this._config.defaultSettings.number_of_shards;
1169
1169
  try {
1170
1170
  await this._client.indices.create(esRequest);
1171
1171
  }
@@ -1174,9 +1174,9 @@ class ES7 {
1174
1174
  "resource_already_exists_exception") {
1175
1175
  // race condition: the indice has been created between the "exists"
1176
1176
  // check above and this "create" attempt, by a request that may not
1177
- // have waited for its shards yet
1178
- await this.waitForCollectionShards(index, collection);
1179
- return null;
1177
+ // have waited for its shards yet. This request's mappings and
1178
+ // settings must still be applied, as if it had seen it exist
1179
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1180
1180
  }
1181
1181
  throw this._esWrapper.formatESError(error);
1182
1182
  }
@@ -1195,6 +1195,15 @@ class ES7 {
1195
1195
  }
1196
1196
  return null;
1197
1197
  }
1198
+ /**
1199
+ * Applies the mappings and settings of a `createCollection` call to a
1200
+ * collection that already exists because another request created it,
1201
+ * maybe so recently that its primary shards are still initializing.
1202
+ */
1203
+ async _updateConcurrentlyCreatedCollection(index, collection, mappings, settings) {
1204
+ await this.waitForCollectionShards(index, collection);
1205
+ return this.updateCollection(index, collection, { mappings, settings });
1206
+ }
1198
1207
  /**
1199
1208
  * Waits until the primary shards of a collection are active, for at most
1200
1209
  * `timeout`, and rejects if they are not by then.
@@ -1205,12 +1214,25 @@ class ES7 {
1205
1214
  * collection waits for its shards (`wait_for_active_shards`); a node that
1206
1215
  * finds it already there calls this instead. It answers at once when the
1207
1216
  * shards are already active.
1217
+ *
1218
+ * A `yellow` status alone does not wait: Elasticsearch reports a new index
1219
+ * whose primaries are still initializing as `yellow` (#3074). Asking for as
1220
+ * many active shard copies as the index has primaries does, and, unlike
1221
+ * `wait_for_no_initializing_shards`, does not also wait for replicas that
1222
+ * are recovering, which a collection with data may take longer to do.
1208
1223
  */
1209
1224
  async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
1225
+ const alias = this._getAlias(index, collection);
1210
1226
  try {
1227
+ const { body } = await this._client.indices.getSettings({
1228
+ index: alias,
1229
+ name: "index.number_of_shards",
1230
+ });
1231
+ const primaries = Object.values(body)[0]?.settings?.index?.number_of_shards;
1211
1232
  await this._client.cluster.health({
1212
- index: this._getAlias(index, collection),
1233
+ index: alias,
1213
1234
  timeout,
1235
+ wait_for_active_shards: String(primaries ?? 1),
1214
1236
  wait_for_status: "yellow",
1215
1237
  });
1216
1238
  }
@@ -443,6 +443,12 @@ export declare class ES8 {
443
443
  mappings?: estypes.MappingTypeMapping;
444
444
  settings?: estypes.IndicesIndexSettings;
445
445
  }): Promise<null>;
446
+ /**
447
+ * Applies the mappings and settings of a `createCollection` call to a
448
+ * collection that already exists because another request created it,
449
+ * maybe so recently that its primary shards are still initializing.
450
+ */
451
+ private _updateConcurrentlyCreatedCollection;
446
452
  /**
447
453
  * Waits until the primary shards of a collection are active, for at most
448
454
  * `timeout`, and rejects if they are not by then.
@@ -453,6 +459,12 @@ export declare class ES8 {
453
459
  * collection waits for its shards (`wait_for_active_shards`); a node that
454
460
  * finds it already there calls this instead. It answers at once when the
455
461
  * shards are already active.
462
+ *
463
+ * A `yellow` status alone does not wait: Elasticsearch reports a new index
464
+ * whose primaries are still initializing as `yellow` (#3074). Asking for as
465
+ * many active shard copies as the index has primaries does, and, unlike
466
+ * `wait_for_no_initializing_shards`, does not also wait for replicas that
467
+ * are recovering, which a collection with data may take longer to do.
456
468
  */
457
469
  waitForCollectionShards(index: string, collection: string, timeout?: string): Promise<void>;
458
470
  /**
@@ -1171,23 +1171,23 @@ class ES8 {
1171
1171
  this._checkDynamicProperty(mappings);
1172
1172
  const exists = await this.hasCollection(index, collection);
1173
1173
  if (exists) {
1174
- // Another node created it, maybe so recently that its primary shards
1175
- // are still initializing
1176
- await this.waitForCollectionShards(index, collection);
1177
- return this.updateCollection(index, collection, { mappings, settings });
1174
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1178
1175
  }
1179
1176
  this._checkMappings(mappings);
1177
+ // Copies, not in-place changes: the race branch below must hand the
1178
+ // caller's own mappings and settings to `updateCollection`
1180
1179
  esRequest.mappings = {
1181
1180
  _meta: mappings._meta || this._config.commonMapping._meta,
1182
1181
  dynamic: mappings.dynamic || this._config.commonMapping.dynamic,
1183
- properties: lodash_1.default.merge(mappings.properties, this._config.commonMapping.properties),
1182
+ properties: lodash_1.default.merge({}, mappings.properties, this._config.commonMapping.properties),
1183
+ };
1184
+ esRequest.settings = {
1185
+ ...settings,
1186
+ number_of_replicas: settings.number_of_replicas ||
1187
+ this._config.defaultSettings.number_of_replicas,
1188
+ number_of_shards: settings.number_of_shards ||
1189
+ this._config.defaultSettings.number_of_shards,
1184
1190
  };
1185
- settings.number_of_replicas =
1186
- settings.number_of_replicas ||
1187
- this._config.defaultSettings.number_of_replicas;
1188
- settings.number_of_shards =
1189
- settings.number_of_shards ||
1190
- this._config.defaultSettings.number_of_shards;
1191
1191
  try {
1192
1192
  await this._client.indices.create(esRequest);
1193
1193
  }
@@ -1196,9 +1196,9 @@ class ES8 {
1196
1196
  "resource_already_exists_exception") {
1197
1197
  // race condition: the indice has been created between the "exists"
1198
1198
  // check above and this "create" attempt, by a request that may not
1199
- // have waited for its shards yet
1200
- await this.waitForCollectionShards(index, collection);
1201
- return null;
1199
+ // have waited for its shards yet. This request's mappings and
1200
+ // settings must still be applied, as if it had seen it exist
1201
+ return this._updateConcurrentlyCreatedCollection(index, collection, mappings, settings);
1202
1202
  }
1203
1203
  throw this._esWrapper.formatESError(error);
1204
1204
  }
@@ -1217,6 +1217,15 @@ class ES8 {
1217
1217
  }
1218
1218
  return null;
1219
1219
  }
1220
+ /**
1221
+ * Applies the mappings and settings of a `createCollection` call to a
1222
+ * collection that already exists because another request created it,
1223
+ * maybe so recently that its primary shards are still initializing.
1224
+ */
1225
+ async _updateConcurrentlyCreatedCollection(index, collection, mappings, settings) {
1226
+ await this.waitForCollectionShards(index, collection);
1227
+ return this.updateCollection(index, collection, { mappings, settings });
1228
+ }
1220
1229
  /**
1221
1230
  * Waits until the primary shards of a collection are active, for at most
1222
1231
  * `timeout`, and rejects if they are not by then.
@@ -1227,12 +1236,25 @@ class ES8 {
1227
1236
  * collection waits for its shards (`wait_for_active_shards`); a node that
1228
1237
  * finds it already there calls this instead. It answers at once when the
1229
1238
  * shards are already active.
1239
+ *
1240
+ * A `yellow` status alone does not wait: Elasticsearch reports a new index
1241
+ * whose primaries are still initializing as `yellow` (#3074). Asking for as
1242
+ * many active shard copies as the index has primaries does, and, unlike
1243
+ * `wait_for_no_initializing_shards`, does not also wait for replicas that
1244
+ * are recovering, which a collection with data may take longer to do.
1230
1245
  */
1231
1246
  async waitForCollectionShards(index, collection, timeout = COLLECTION_SHARDS_TIMEOUT) {
1247
+ const alias = this._getAlias(index, collection);
1232
1248
  try {
1249
+ const body = await this._client.indices.getSettings({
1250
+ index: alias,
1251
+ name: "index.number_of_shards",
1252
+ });
1253
+ const primaries = Object.values(body)[0]?.settings?.index?.number_of_shards;
1233
1254
  await this._client.cluster.health({
1234
- index: this._getAlias(index, collection),
1255
+ index: alias,
1235
1256
  timeout,
1257
+ wait_for_active_shards: Number(primaries ?? 1),
1236
1258
  wait_for_status: "yellow",
1237
1259
  });
1238
1260
  }
@@ -163,6 +163,13 @@ export interface IKuzzleConfiguration {
163
163
  * Optional here for the reason given on {@link IKuzzleConfiguration.version}.
164
164
  */
165
165
  allowAllOrigins?: boolean;
166
+ /**
167
+ * Derived at startup: the deprecated configuration keys the user set
168
+ * (today, those of "security.jwt"), logged as a warning.
169
+ *
170
+ * Optional here for the reason given on {@link IKuzzleConfiguration.version}.
171
+ */
172
+ deprecatedSettings?: string[];
166
173
  };
167
174
  /**
168
175
  * Collection specifications to apply at startup, by index then collection.
@@ -21,9 +21,10 @@ export type SecurityConfiguration = {
21
21
  /**
22
22
  * @deprecated Use `security.authToken` instead.
23
23
  *
24
- * `default.config.ts` always ships it, and `authToken`'s readers fall back
25
- * to it — `authToken.algorithm ?? jwt.algorithm`. Optional all the same, as
26
- * v2.56.0 declared it: a required member breaks code that builds this type.
24
+ * Each setting the configuration sets here applies unless `authToken` sets
25
+ * it too (`loadConfig()` resolves both sections to the values in effect,
26
+ * see `authTokenSetting`). Optional all the same, as v2.56.0 declared it:
27
+ * a required member breaks code that builds this type.
27
28
  */
28
29
  jwt?: JSONObject;
29
30
  /**
@@ -54,8 +54,8 @@ export type ControllerDefinition = {
54
54
  handler: (request: KuzzleRequest) => Promise<any>;
55
55
  /**
56
56
  * Declare HTTP routes (optional).
57
- * Http routes will be auto-generated unless at least one is provided
58
- * or an empty array is provided.
57
+ * When omitted or empty, a default route is generated:
58
+ * `GET /_/<controller>/<action>`, both names in kebab-case.
59
59
  */
60
60
  http?: HttpRoute[];
61
61
  };
@@ -71,8 +71,8 @@ export type HttpRoute = {
71
71
  verb: "get" | "head" | "post" | "put" | "delete" | "patch" | "options";
72
72
  /**
73
73
  * Route path.
74
- * A route starting with `/` will be prefixed by `/_` otherwise the route
75
- * will be prefixed by `/_/<application-name>/`.
74
+ * A route starting with `/` is used as is, any other route is prefixed
75
+ * with `/_/`.
76
76
  */
77
77
  path: string;
78
78
  /**
@@ -86,9 +86,9 @@ export declare class Mutex {
86
86
  *
87
87
  * @return {Promise.<boolean>} True if the ressource has been unlocked before the `timeout`
88
88
  */
89
- wait({ attemptDelay, timeout, }: {
90
- attemptDelay?: number | undefined;
91
- timeout?: number | undefined;
89
+ wait({ attemptDelay, timeout, }?: {
90
+ attemptDelay?: number;
91
+ timeout?: number;
92
92
  }): Promise<boolean>;
93
93
  /**
94
94
  * Frees every lock this process holds, and stops it from taking new ones.
@@ -196,7 +196,7 @@ class Mutex {
196
196
  *
197
197
  * @return {Promise.<boolean>} True if the ressource has been unlocked before the `timeout`
198
198
  */
199
- async wait({ attemptDelay = this.attemptDelay, timeout = this.timeout, }) {
199
+ async wait({ attemptDelay = this.attemptDelay, timeout = this.timeout, } = {}) {
200
200
  let duration = 0;
201
201
  let isLocked;
202
202
  do {
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kuzzle",
3
3
  "author": "The Kuzzle Team <support@kuzzle.io>",
4
- "version": "2.60.0-beta.4",
4
+ "version": "2.60.0-beta.6",
5
5
  "description": "Kuzzle is an open-source solution that handles all the data management through a secured API, with a large choice of protocols.",
6
6
  "scripts": {
7
7
  "build": "rm -Rf ./dist && tsc && node -r ts-node/register/transpile-only ./bin/copy-binaries.ts",