kuzzle 2.58.0 → 2.59.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/dist/lib/cluster/node.js +10 -0
- package/dist/lib/core/storage/clientAdapter.d.ts +35 -1
- package/dist/lib/core/storage/clientAdapter.js +159 -0
- package/dist/lib/core/storage/indexCache.d.ts +49 -2
- package/dist/lib/core/storage/indexCache.js +68 -1
- package/dist/lib/kerror/codes/1-services.json +6 -0
- package/dist/lib/service/storage/8/elasticsearch.d.ts +4 -0
- package/dist/lib/service/storage/8/elasticsearch.js +24 -3
- package/dist/lib/types/Policy.d.ts +6 -0
- package/dist/lib/types/RoleDefinition.d.ts +4 -8
- package/dist/lib/types/storage/7/Elasticsearch.d.ts +1 -21
- package/dist/lib/types/storage/8/Elasticsearch.d.ts +1 -21
- package/dist/lib/types/storage/KuzzleInfo.d.ts +25 -0
- package/dist/lib/types/storage/KuzzleInfo.js +5 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/lib/cluster/node.js
CHANGED
|
@@ -50,6 +50,9 @@ const publisher_1 = __importDefault(require("./publisher"));
|
|
|
50
50
|
const state_1 = __importDefault(require("./state"));
|
|
51
51
|
const subscriber_1 = __importDefault(require("./subscriber"));
|
|
52
52
|
const debug = (0, debug_1.default)("kuzzle:cluster:sync");
|
|
53
|
+
/** Cluster-wide events carrying the collection write locks (see registerEvents) */
|
|
54
|
+
const COLLECTION_LOCK_EVENT = "core:storage:collection:lock";
|
|
55
|
+
const COLLECTION_UNLOCK_EVENT = "core:storage:collection:unlock";
|
|
53
56
|
/**
|
|
54
57
|
* How long `addNode()` may wait for a joining node's first sync message before
|
|
55
58
|
* it answers that node's handshake anyway.
|
|
@@ -888,6 +891,13 @@ class ClusterNode {
|
|
|
888
891
|
global.kuzzle.on("core:storage:collection:delete:after", ({ collection, index, scope }) => {
|
|
889
892
|
this.onCollectionRemoved(scope, index, collection);
|
|
890
893
|
});
|
|
894
|
+
// Collection write locks travel as cluster-wide events rather than as
|
|
895
|
+
// dedicated sync topics: a node evicts the sender of a topic it does not
|
|
896
|
+
// know, which would turn a rolling upgrade into evictions.
|
|
897
|
+
global.kuzzle.on("core:storage:collection:lock:after", (payload) => this.broadcast(COLLECTION_LOCK_EVENT, payload));
|
|
898
|
+
global.kuzzle.on("core:storage:collection:unlock:after", (payload) => this.broadcast(COLLECTION_UNLOCK_EVENT, payload));
|
|
899
|
+
this.eventEmitter.on(COLLECTION_LOCK_EVENT, ({ collection, index, lock, scope }) => global.kuzzle.ask(`core:storage:${scope}:cache:setLock`, index, collection, lock));
|
|
900
|
+
this.eventEmitter.on(COLLECTION_UNLOCK_EVENT, ({ collection, index, scope }) => global.kuzzle.ask(`core:storage:${scope}:cache:removeLock`, index, collection));
|
|
891
901
|
}
|
|
892
902
|
/**
|
|
893
903
|
* Triggered whenever a realtime room is created on this node
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSONObject } from "../../types/JSONObject";
|
|
2
2
|
import { Elasticsearch } from "../../service/storage/Elasticsearch";
|
|
3
|
-
import { IndexCache } from "./indexCache";
|
|
3
|
+
import { IndexCache, type CollectionLock } from "./indexCache";
|
|
4
4
|
import type { storeScopeEnum } from "./storeScopeEnum";
|
|
5
5
|
import "../../types/Global";
|
|
6
6
|
/** Where `createIndex` / `createCollection` / `loadMappings` write, and whether
|
|
@@ -43,10 +43,44 @@ declare class ClientAdapter {
|
|
|
43
43
|
* Also checks for duplicated index names.
|
|
44
44
|
*/
|
|
45
45
|
populateCache(): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Replaces the cached collection write locks with the cluster-wide ones,
|
|
48
|
+
* stored in Redis.
|
|
49
|
+
*/
|
|
50
|
+
loadLocks(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Write-locks a collection cluster-wide: until it is unlocked, every action
|
|
53
|
+
* writing to it is rejected with `services.storage.collection_locked`, and
|
|
54
|
+
* reads are served as usual. Locking a collection again with the same owner
|
|
55
|
+
* refreshes the lock.
|
|
56
|
+
*
|
|
57
|
+
* The other nodes learn about the lock asynchronously (cluster sync): a write
|
|
58
|
+
* they accepted just before may still land.
|
|
59
|
+
*
|
|
60
|
+
* @throws If the collection does not exist, or is locked by another owner
|
|
61
|
+
*/
|
|
62
|
+
lockCollection(index: string, collection: string, { owner, reason }: {
|
|
63
|
+
owner: string;
|
|
64
|
+
reason: string;
|
|
65
|
+
}): Promise<CollectionLock>;
|
|
66
|
+
/**
|
|
67
|
+
* Releases a collection write lock.
|
|
68
|
+
*
|
|
69
|
+
* @returns false if the collection was not locked
|
|
70
|
+
* @throws If the collection is locked by another owner
|
|
71
|
+
*/
|
|
72
|
+
unlockCollection(index: string, collection: string, owner: string): Promise<boolean>;
|
|
73
|
+
private get locksKey();
|
|
74
|
+
private fetchLock;
|
|
75
|
+
/**
|
|
76
|
+
* @throws If `lock` is held by someone other than `owner`
|
|
77
|
+
*/
|
|
78
|
+
private assertLockOwner;
|
|
46
79
|
registerCollectionEvents(): void;
|
|
47
80
|
registerIndexEvents(): void;
|
|
48
81
|
registerDocumentEvents(): void;
|
|
49
82
|
registerMappingEvents(): void;
|
|
83
|
+
registerLockEvents(): void;
|
|
50
84
|
/**
|
|
51
85
|
* Cache update operations. These events trigger neither any actual change
|
|
52
86
|
* in the storage layer, nor kuzzle events.
|
|
@@ -49,6 +49,12 @@ const kerror = __importStar(require("../../kerror"));
|
|
|
49
49
|
const mutex_1 = require("../../util/mutex"); // NOSONAR: see loadMappings
|
|
50
50
|
require("../../types/Global");
|
|
51
51
|
const servicesError = kerror.wrap("services", "storage");
|
|
52
|
+
/**
|
|
53
|
+
* Redis hash holding the collection write locks of a storage scope, the
|
|
54
|
+
* cluster-wide source of truth that each node mirrors in its index cache.
|
|
55
|
+
* One field per locked collection: `JSON.stringify([index, collection])`.
|
|
56
|
+
*/
|
|
57
|
+
const LOCKS_KEY_PREFIX = "{core/storage}/collection-locks/";
|
|
52
58
|
/** Hoisted out of the signature: an object literal as a default parameter is
|
|
53
59
|
* re-allocated on every call, and this one is only ever read. */
|
|
54
60
|
const DEFAULT_LOAD_MAPPINGS_OPTIONS = Object.freeze({
|
|
@@ -76,6 +82,11 @@ class ClientAdapter {
|
|
|
76
82
|
this.registerDocumentEvents();
|
|
77
83
|
this.registerMappingEvents();
|
|
78
84
|
this.registerCacheEvents();
|
|
85
|
+
this.registerLockEvents();
|
|
86
|
+
// The locks were first loaded with the cache, before this node joined the
|
|
87
|
+
// cluster: one taken in between was broadcast before this node could hear
|
|
88
|
+
// it. Reading them again once the cluster is joined closes that gap.
|
|
89
|
+
global.kuzzle.onPipe("kuzzle:state:live", () => this.loadLocks());
|
|
79
90
|
// Global store events registration
|
|
80
91
|
/**
|
|
81
92
|
* Manually refresh the index cache (e.g. after alias creation)
|
|
@@ -111,6 +122,8 @@ class ClientAdapter {
|
|
|
111
122
|
}
|
|
112
123
|
async createCollection(index, collection, opts, { indexCacheOnly = false, propagate = true } = {}) {
|
|
113
124
|
if (!indexCacheOnly) {
|
|
125
|
+
// Creating an existing collection updates its mappings and settings
|
|
126
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
114
127
|
await this.client.createCollection(index, collection, opts);
|
|
115
128
|
}
|
|
116
129
|
this.cache.addCollection(index, collection);
|
|
@@ -124,6 +137,7 @@ class ClientAdapter {
|
|
|
124
137
|
}
|
|
125
138
|
async deleteIndex(index) {
|
|
126
139
|
this.cache.assertIndexExists(index);
|
|
140
|
+
this.cache.assertIndexWritable(index);
|
|
127
141
|
await this.client.deleteIndex(index);
|
|
128
142
|
this.cache.removeIndex(index);
|
|
129
143
|
global.kuzzle.emit("core:storage:index:delete:after", {
|
|
@@ -134,6 +148,7 @@ class ClientAdapter {
|
|
|
134
148
|
async deleteIndexes(indexes) {
|
|
135
149
|
for (const index of indexes) {
|
|
136
150
|
this.cache.assertIndexExists(index);
|
|
151
|
+
this.cache.assertIndexWritable(index);
|
|
137
152
|
}
|
|
138
153
|
const deleted = await this.client.deleteIndexes(indexes);
|
|
139
154
|
if (deleted.length > 0) {
|
|
@@ -149,6 +164,7 @@ class ClientAdapter {
|
|
|
149
164
|
}
|
|
150
165
|
async deleteCollection(index, collection) {
|
|
151
166
|
this.cache.assertCollectionExists(index, collection);
|
|
167
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
152
168
|
await this.client.deleteCollection(index, collection);
|
|
153
169
|
this.cache.removeCollection(index, collection);
|
|
154
170
|
global.kuzzle.emit("core:storage:collection:delete:after", {
|
|
@@ -174,6 +190,88 @@ class ClientAdapter {
|
|
|
174
190
|
this.cache.addCollection(index, collection);
|
|
175
191
|
}
|
|
176
192
|
}
|
|
193
|
+
await this.loadLocks();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Replaces the cached collection write locks with the cluster-wide ones,
|
|
197
|
+
* stored in Redis.
|
|
198
|
+
*/
|
|
199
|
+
async loadLocks() {
|
|
200
|
+
const fields = await global.kuzzle.ask("core:cache:internal:execute", "hgetall", this.locksKey);
|
|
201
|
+
this.cache.clearLocks();
|
|
202
|
+
for (const [field, value] of Object.entries(fields ?? {})) {
|
|
203
|
+
const [index, collection] = JSON.parse(field);
|
|
204
|
+
this.cache.setLock(index, collection, JSON.parse(value));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Write-locks a collection cluster-wide: until it is unlocked, every action
|
|
209
|
+
* writing to it is rejected with `services.storage.collection_locked`, and
|
|
210
|
+
* reads are served as usual. Locking a collection again with the same owner
|
|
211
|
+
* refreshes the lock.
|
|
212
|
+
*
|
|
213
|
+
* The other nodes learn about the lock asynchronously (cluster sync): a write
|
|
214
|
+
* they accepted just before may still land.
|
|
215
|
+
*
|
|
216
|
+
* @throws If the collection does not exist, or is locked by another owner
|
|
217
|
+
*/
|
|
218
|
+
async lockCollection(index, collection, { owner, reason }) {
|
|
219
|
+
this.cache.assertCollectionExists(index, collection);
|
|
220
|
+
const field = JSON.stringify([index, collection]);
|
|
221
|
+
const lock = { lockedAt: Date.now(), owner, reason };
|
|
222
|
+
// Redis decides, not the local copy: two nodes locking the same collection
|
|
223
|
+
// at once must not both win.
|
|
224
|
+
const created = await global.kuzzle.ask("core:cache:internal:execute", "hsetnx", this.locksKey, field, JSON.stringify(lock));
|
|
225
|
+
if (created === 0) {
|
|
226
|
+
this.assertLockOwner(index, collection, await this.fetchLock(field), owner);
|
|
227
|
+
await global.kuzzle.ask("core:cache:internal:execute", "hset", this.locksKey, field, JSON.stringify(lock));
|
|
228
|
+
}
|
|
229
|
+
this.cache.setLock(index, collection, lock);
|
|
230
|
+
global.kuzzle.emit("core:storage:collection:lock:after", {
|
|
231
|
+
collection,
|
|
232
|
+
index,
|
|
233
|
+
lock,
|
|
234
|
+
scope: this.scope,
|
|
235
|
+
});
|
|
236
|
+
return lock;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Releases a collection write lock.
|
|
240
|
+
*
|
|
241
|
+
* @returns false if the collection was not locked
|
|
242
|
+
* @throws If the collection is locked by another owner
|
|
243
|
+
*/
|
|
244
|
+
async unlockCollection(index, collection, owner) {
|
|
245
|
+
const field = JSON.stringify([index, collection]);
|
|
246
|
+
const current = await this.fetchLock(field);
|
|
247
|
+
if (current === null) {
|
|
248
|
+
this.cache.removeLock(index, collection);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
this.assertLockOwner(index, collection, current, owner);
|
|
252
|
+
await global.kuzzle.ask("core:cache:internal:execute", "hdel", this.locksKey, field);
|
|
253
|
+
this.cache.removeLock(index, collection);
|
|
254
|
+
global.kuzzle.emit("core:storage:collection:unlock:after", {
|
|
255
|
+
collection,
|
|
256
|
+
index,
|
|
257
|
+
scope: this.scope,
|
|
258
|
+
});
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
get locksKey() {
|
|
262
|
+
return `${LOCKS_KEY_PREFIX}${this.scope}`;
|
|
263
|
+
}
|
|
264
|
+
async fetchLock(field) {
|
|
265
|
+
const value = await global.kuzzle.ask("core:cache:internal:execute", "hget", this.locksKey, field);
|
|
266
|
+
return value === null ? null : JSON.parse(value);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* @throws If `lock` is held by someone other than `owner`
|
|
270
|
+
*/
|
|
271
|
+
assertLockOwner(index, collection, lock, owner) {
|
|
272
|
+
if (lock !== null && lock.owner !== owner) {
|
|
273
|
+
throw servicesError.get("collection_locked", index, collection, lock.owner, lock.reason);
|
|
274
|
+
}
|
|
177
275
|
}
|
|
178
276
|
registerCollectionEvents() {
|
|
179
277
|
/**
|
|
@@ -233,6 +331,7 @@ class ClientAdapter {
|
|
|
233
331
|
*/
|
|
234
332
|
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:truncate`, (index, collection) => {
|
|
235
333
|
this.cache.assertCollectionExists(index, collection);
|
|
334
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
236
335
|
return this.client.truncateCollection(index, collection);
|
|
237
336
|
});
|
|
238
337
|
/**
|
|
@@ -244,6 +343,7 @@ class ClientAdapter {
|
|
|
244
343
|
*/
|
|
245
344
|
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:update`, (index, collection, changes) => {
|
|
246
345
|
this.cache.assertCollectionExists(index, collection);
|
|
346
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
247
347
|
return this.client.updateCollection(index, collection, changes);
|
|
248
348
|
});
|
|
249
349
|
}
|
|
@@ -299,6 +399,7 @@ class ClientAdapter {
|
|
|
299
399
|
*/
|
|
300
400
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:bulk`, (index, collection, bulk, opts) => {
|
|
301
401
|
this.cache.assertCollectionExists(index, collection);
|
|
402
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
302
403
|
return this.client.import(index, collection, bulk, opts);
|
|
303
404
|
});
|
|
304
405
|
/**
|
|
@@ -324,6 +425,7 @@ class ClientAdapter {
|
|
|
324
425
|
*/
|
|
325
426
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:create`, (index, collection, content, opts) => {
|
|
326
427
|
this.cache.assertCollectionExists(index, collection);
|
|
428
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
327
429
|
return this.client.create(index, collection, content, opts);
|
|
328
430
|
});
|
|
329
431
|
/**
|
|
@@ -338,6 +440,7 @@ class ClientAdapter {
|
|
|
338
440
|
*/
|
|
339
441
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:createOrReplace`, (index, collection, id, content, opts) => {
|
|
340
442
|
this.cache.assertCollectionExists(index, collection);
|
|
443
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
341
444
|
return this.client.createOrReplace(index, collection, id, content, opts);
|
|
342
445
|
});
|
|
343
446
|
/**
|
|
@@ -351,6 +454,7 @@ class ClientAdapter {
|
|
|
351
454
|
*/
|
|
352
455
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:delete`, (index, collection, id, opts) => {
|
|
353
456
|
this.cache.assertCollectionExists(index, collection);
|
|
457
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
354
458
|
return this.client.delete(index, collection, id, opts);
|
|
355
459
|
});
|
|
356
460
|
/**
|
|
@@ -364,6 +468,7 @@ class ClientAdapter {
|
|
|
364
468
|
*/
|
|
365
469
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:deleteByQuery`, (index, collection, query, opts) => {
|
|
366
470
|
this.cache.assertCollectionExists(index, collection);
|
|
471
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
367
472
|
return this.client.deleteByQuery(index, collection, query, opts);
|
|
368
473
|
});
|
|
369
474
|
/**
|
|
@@ -378,6 +483,7 @@ class ClientAdapter {
|
|
|
378
483
|
*/
|
|
379
484
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:deleteFields`, (index, collection, id, fields, opts) => {
|
|
380
485
|
this.cache.assertCollectionExists(index, collection);
|
|
486
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
381
487
|
return this.client.deleteFields(index, collection, id, fields, opts);
|
|
382
488
|
});
|
|
383
489
|
/**
|
|
@@ -433,6 +539,7 @@ class ClientAdapter {
|
|
|
433
539
|
*/
|
|
434
540
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mCreate`, (index, collection, documents, opts) => {
|
|
435
541
|
this.cache.assertCollectionExists(index, collection);
|
|
542
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
436
543
|
return this.client.mCreate(index, collection, documents, opts);
|
|
437
544
|
});
|
|
438
545
|
/**
|
|
@@ -446,6 +553,7 @@ class ClientAdapter {
|
|
|
446
553
|
*/
|
|
447
554
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mCreateOrReplace`, (index, collection, documents, opts) => {
|
|
448
555
|
this.cache.assertCollectionExists(index, collection);
|
|
556
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
449
557
|
return this.client.mCreateOrReplace(index, collection, documents, opts);
|
|
450
558
|
});
|
|
451
559
|
/**
|
|
@@ -459,6 +567,7 @@ class ClientAdapter {
|
|
|
459
567
|
*/
|
|
460
568
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mDelete`, (index, collection, ids, opts) => {
|
|
461
569
|
this.cache.assertCollectionExists(index, collection);
|
|
570
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
462
571
|
return this.client.mDelete(index, collection, ids, opts);
|
|
463
572
|
});
|
|
464
573
|
/**
|
|
@@ -472,6 +581,7 @@ class ClientAdapter {
|
|
|
472
581
|
*/
|
|
473
582
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mReplace`, (index, collection, documents, opts) => {
|
|
474
583
|
this.cache.assertCollectionExists(index, collection);
|
|
584
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
475
585
|
return this.client.mReplace(index, collection, documents, opts);
|
|
476
586
|
});
|
|
477
587
|
/**
|
|
@@ -485,6 +595,7 @@ class ClientAdapter {
|
|
|
485
595
|
*/
|
|
486
596
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mUpdate`, (index, collection, documents, opts) => {
|
|
487
597
|
this.cache.assertCollectionExists(index, collection);
|
|
598
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
488
599
|
return this.client.mUpdate(index, collection, documents, opts);
|
|
489
600
|
});
|
|
490
601
|
/**
|
|
@@ -499,6 +610,7 @@ class ClientAdapter {
|
|
|
499
610
|
*/
|
|
500
611
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:mUpsert`, (index, collection, documents, opts) => {
|
|
501
612
|
this.cache.assertCollectionExists(index, collection);
|
|
613
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
502
614
|
return this.client.mUpsert(index, collection, documents, opts);
|
|
503
615
|
});
|
|
504
616
|
/**
|
|
@@ -539,6 +651,7 @@ class ClientAdapter {
|
|
|
539
651
|
*/
|
|
540
652
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:replace`, (index, collection, id, content, opts) => {
|
|
541
653
|
this.cache.assertCollectionExists(index, collection);
|
|
654
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
542
655
|
return this.client.replace(index, collection, id, content, opts);
|
|
543
656
|
});
|
|
544
657
|
/**
|
|
@@ -590,6 +703,7 @@ class ClientAdapter {
|
|
|
590
703
|
*/
|
|
591
704
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:update`, (index, collection, id, content, opts) => {
|
|
592
705
|
this.cache.assertCollectionExists(index, collection);
|
|
706
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
593
707
|
return this.client.update(index, collection, id, content, opts);
|
|
594
708
|
});
|
|
595
709
|
/**
|
|
@@ -605,6 +719,7 @@ class ClientAdapter {
|
|
|
605
719
|
*/
|
|
606
720
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:updateByQuery`, (index, collection, query, changes, opts) => {
|
|
607
721
|
this.cache.assertCollectionExists(index, collection);
|
|
722
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
608
723
|
return this.client.updateByQuery(index, collection, query, changes, opts);
|
|
609
724
|
});
|
|
610
725
|
/**
|
|
@@ -620,6 +735,7 @@ class ClientAdapter {
|
|
|
620
735
|
*/
|
|
621
736
|
global.kuzzle.onAsk(`core:storage:${this.scope}:bulk:updateByQuery`, (index, collection, query, changes, opts) => {
|
|
622
737
|
this.cache.assertCollectionExists(index, collection);
|
|
738
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
623
739
|
return this.client.bulkUpdateByQuery(index, collection, query, changes, opts);
|
|
624
740
|
});
|
|
625
741
|
/**
|
|
@@ -635,6 +751,7 @@ class ClientAdapter {
|
|
|
635
751
|
*/
|
|
636
752
|
global.kuzzle.onAsk(`core:storage:${this.scope}:document:upsert`, (index, collection, id, content, opts) => {
|
|
637
753
|
this.cache.assertCollectionExists(index, collection);
|
|
754
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
638
755
|
return this.client.upsert(index, collection, id, content, opts);
|
|
639
756
|
});
|
|
640
757
|
}
|
|
@@ -671,9 +788,37 @@ class ClientAdapter {
|
|
|
671
788
|
*/
|
|
672
789
|
global.kuzzle.onAsk(`core:storage:${this.scope}:mappings:update`, (index, collection, mappings) => {
|
|
673
790
|
this.cache.assertCollectionExists(index, collection);
|
|
791
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
674
792
|
return this.client.updateMapping(index, collection, mappings);
|
|
675
793
|
});
|
|
676
794
|
}
|
|
795
|
+
registerLockEvents() {
|
|
796
|
+
/**
|
|
797
|
+
* Write-lock a collection, cluster-wide
|
|
798
|
+
* @param {string} index
|
|
799
|
+
* @param {string} collection
|
|
800
|
+
* @param {Object} options -- { owner, reason }
|
|
801
|
+
* @returns {Promise.<{ owner, reason, lockedAt }>}
|
|
802
|
+
* @throws If the collection does not exist, or is locked by another owner
|
|
803
|
+
*/
|
|
804
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:lock`, (index, collection, options) => this.lockCollection(index, collection, options));
|
|
805
|
+
/**
|
|
806
|
+
* Release a collection write lock
|
|
807
|
+
* @param {string} index
|
|
808
|
+
* @param {string} collection
|
|
809
|
+
* @param {string} owner
|
|
810
|
+
* @returns {Promise.<boolean>} false if the collection was not locked
|
|
811
|
+
* @throws If the collection is locked by another owner
|
|
812
|
+
*/
|
|
813
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:unlock`, (index, collection, owner) => this.unlockCollection(index, collection, owner));
|
|
814
|
+
/**
|
|
815
|
+
* Return a collection write lock
|
|
816
|
+
* @param {string} index
|
|
817
|
+
* @param {string} collection
|
|
818
|
+
* @returns {{ owner, reason, lockedAt }|null}
|
|
819
|
+
*/
|
|
820
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:collection:lock:get`, (index, collection) => this.cache.getLock(index, collection));
|
|
821
|
+
}
|
|
677
822
|
/**
|
|
678
823
|
* Cache update operations. These events trigger neither any actual change
|
|
679
824
|
* in the storage layer, nor kuzzle events.
|
|
@@ -705,6 +850,19 @@ class ClientAdapter {
|
|
|
705
850
|
* @param {string} collection
|
|
706
851
|
*/
|
|
707
852
|
global.kuzzle.onAsk(`core:storage:${this.scope}:cache:removeCollection`, (index, collection) => this.cache.removeCollection(index, collection));
|
|
853
|
+
/**
|
|
854
|
+
* Caches a collection write lock taken on another node
|
|
855
|
+
* @param {string} index
|
|
856
|
+
* @param {string} collection
|
|
857
|
+
* @param {Object} lock -- { owner, reason, lockedAt }
|
|
858
|
+
*/
|
|
859
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:cache:setLock`, (index, collection, lock) => this.cache.setLock(index, collection, lock));
|
|
860
|
+
/**
|
|
861
|
+
* Removes a collection write lock released on another node
|
|
862
|
+
* @param {string} index
|
|
863
|
+
* @param {string} collection
|
|
864
|
+
*/
|
|
865
|
+
global.kuzzle.onAsk(`core:storage:${this.scope}:cache:removeLock`, (index, collection) => this.cache.removeLock(index, collection));
|
|
708
866
|
}
|
|
709
867
|
/**
|
|
710
868
|
* Load database fixtures into Kuzzle
|
|
@@ -725,6 +883,7 @@ class ClientAdapter {
|
|
|
725
883
|
}
|
|
726
884
|
for (const [collection, payload] of Object.entries(collections)) {
|
|
727
885
|
this.cache.assertCollectionExists(index, collection);
|
|
886
|
+
this.cache.assertCollectionWritable(index, collection);
|
|
728
887
|
const { errors } = await this.client.import(index, collection, payload, { refresh });
|
|
729
888
|
if (errors.length > 0) {
|
|
730
889
|
throw servicesError.get("import_failed", errors);
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A collection write lock: while it is held, the storage layer rejects every
|
|
3
|
+
* action writing to the collection, and serves reads as usual.
|
|
4
|
+
*/
|
|
5
|
+
export interface CollectionLock {
|
|
6
|
+
/** Who holds the lock (e.g. a plugin name), the only one who can release it */
|
|
7
|
+
owner: string;
|
|
8
|
+
/** Why the collection is locked, quoted in the error writers receive */
|
|
9
|
+
reason: string;
|
|
10
|
+
/** Lock timestamp (Epoch-millis) */
|
|
11
|
+
lockedAt: number;
|
|
12
|
+
}
|
|
1
13
|
export declare class IndexCache {
|
|
2
14
|
/**
|
|
3
15
|
* Index map: each entry holds a set of collection names
|
|
4
16
|
*
|
|
5
17
|
* Map<index, Set<collection>>
|
|
6
18
|
*/
|
|
7
|
-
private indexes;
|
|
8
|
-
|
|
19
|
+
private readonly indexes;
|
|
20
|
+
/**
|
|
21
|
+
* Collection write locks, a local copy of the cluster-wide state (see
|
|
22
|
+
* `ClientAdapter`): checking a lock on every write must not cost a round
|
|
23
|
+
* trip to Redis.
|
|
24
|
+
*
|
|
25
|
+
* Map<index, Map<collection, CollectionLock>>
|
|
26
|
+
*/
|
|
27
|
+
private readonly locks;
|
|
9
28
|
/**
|
|
10
29
|
* Cache a new index
|
|
11
30
|
*
|
|
@@ -60,4 +79,32 @@ export declare class IndexCache {
|
|
|
60
79
|
* Assert that the provided index and collection exist
|
|
61
80
|
*/
|
|
62
81
|
assertCollectionExists(index: string, collection: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Cache a collection write lock, replacing the one it may already hold
|
|
84
|
+
*/
|
|
85
|
+
setLock(index: string, collection: string, lock: CollectionLock): void;
|
|
86
|
+
/**
|
|
87
|
+
* Remove a collection write lock from the cache
|
|
88
|
+
*/
|
|
89
|
+
removeLock(index: string, collection: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Remove every cached collection write lock
|
|
92
|
+
*/
|
|
93
|
+
clearLocks(): void;
|
|
94
|
+
/**
|
|
95
|
+
* Return a collection write lock, or null if the collection is not locked
|
|
96
|
+
*/
|
|
97
|
+
getLock(index: string, collection: string): CollectionLock | null;
|
|
98
|
+
/**
|
|
99
|
+
* Assert that the provided collection is not write-locked
|
|
100
|
+
*
|
|
101
|
+
* @throws If the collection is locked
|
|
102
|
+
*/
|
|
103
|
+
assertCollectionWritable(index: string, collection: string): void;
|
|
104
|
+
/**
|
|
105
|
+
* Assert that none of the provided index collections is write-locked
|
|
106
|
+
*
|
|
107
|
+
* @throws If one of the index collections is locked
|
|
108
|
+
*/
|
|
109
|
+
assertIndexWritable(index: string): void;
|
|
63
110
|
}
|
|
@@ -54,7 +54,14 @@ class IndexCache {
|
|
|
54
54
|
* Map<index, Set<collection>>
|
|
55
55
|
*/
|
|
56
56
|
this.indexes = new Map();
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Collection write locks, a local copy of the cluster-wide state (see
|
|
59
|
+
* `ClientAdapter`): checking a lock on every write must not cost a round
|
|
60
|
+
* trip to Redis.
|
|
61
|
+
*
|
|
62
|
+
* Map<index, Map<collection, CollectionLock>>
|
|
63
|
+
*/
|
|
64
|
+
this.locks = new Map();
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* Cache a new index
|
|
@@ -150,6 +157,66 @@ class IndexCache {
|
|
|
150
157
|
throw storageError.get("unknown_collection", index, collection);
|
|
151
158
|
}
|
|
152
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Cache a collection write lock, replacing the one it may already hold
|
|
162
|
+
*/
|
|
163
|
+
setLock(index, collection, lock) {
|
|
164
|
+
let collections = this.locks.get(index);
|
|
165
|
+
if (collections === undefined) {
|
|
166
|
+
collections = new Map();
|
|
167
|
+
this.locks.set(index, collections);
|
|
168
|
+
}
|
|
169
|
+
collections.set(collection, lock);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Remove a collection write lock from the cache
|
|
173
|
+
*/
|
|
174
|
+
removeLock(index, collection) {
|
|
175
|
+
const collections = this.locks.get(index);
|
|
176
|
+
if (collections) {
|
|
177
|
+
collections.delete(collection);
|
|
178
|
+
if (collections.size === 0) {
|
|
179
|
+
this.locks.delete(index);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Remove every cached collection write lock
|
|
185
|
+
*/
|
|
186
|
+
clearLocks() {
|
|
187
|
+
this.locks.clear();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Return a collection write lock, or null if the collection is not locked
|
|
191
|
+
*/
|
|
192
|
+
getLock(index, collection) {
|
|
193
|
+
return this.locks.get(index)?.get(collection) ?? null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Assert that the provided collection is not write-locked
|
|
197
|
+
*
|
|
198
|
+
* @throws If the collection is locked
|
|
199
|
+
*/
|
|
200
|
+
assertCollectionWritable(index, collection) {
|
|
201
|
+
const lock = this.getLock(index, collection);
|
|
202
|
+
if (lock !== null) {
|
|
203
|
+
throw storageError.get("collection_locked", index, collection, lock.owner, lock.reason);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Assert that none of the provided index collections is write-locked
|
|
208
|
+
*
|
|
209
|
+
* @throws If one of the index collections is locked
|
|
210
|
+
*/
|
|
211
|
+
assertIndexWritable(index) {
|
|
212
|
+
const collections = this.locks.get(index);
|
|
213
|
+
if (collections === undefined) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
for (const collection of collections.keys()) {
|
|
217
|
+
this.assertCollectionWritable(index, collection);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
153
220
|
}
|
|
154
221
|
exports.IndexCache = IndexCache;
|
|
155
222
|
//# sourceMappingURL=indexCache.js.map
|
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"message": "Attempt to recreate an existing collection %s of index %s of scope %s with non matching static setting : %s at %s while existing one is at %s",
|
|
294
294
|
"code": 51,
|
|
295
295
|
"class": "BadRequestError"
|
|
296
|
+
},
|
|
297
|
+
"collection_locked": {
|
|
298
|
+
"description": "The collection is locked against writes, typically during a maintenance operation such as a mapping migration. Reads are still served: retry the write once the lock is released",
|
|
299
|
+
"message": "The collection \"%s\":\"%s\" is locked by \"%s\" (%s): write actions are rejected until it is unlocked.",
|
|
300
|
+
"code": 52,
|
|
301
|
+
"class": "ServiceUnavailableError"
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
},
|
|
@@ -877,6 +877,10 @@ export declare class ES8 {
|
|
|
877
877
|
* applies, which is what a row without one used to throw on.
|
|
878
878
|
*/
|
|
879
879
|
_catAliases(name?: string): Promise<CatAliasRecord[]>;
|
|
880
|
+
/**
|
|
881
|
+
* Returns the indices marked with `UNMANAGED_INDICE_ALIAS`.
|
|
882
|
+
*/
|
|
883
|
+
_listUnmanagedIndices(): Promise<Set<string>>;
|
|
880
884
|
/**
|
|
881
885
|
* Check for each indice whether it has an alias or not.
|
|
882
886
|
* When the latter is missing, create one based on the indice name.
|
|
@@ -79,6 +79,14 @@ const PUBLIC_PREFIX = "&";
|
|
|
79
79
|
const INDEX_PREFIX_POSITION_IN_INDICE = 0;
|
|
80
80
|
const INDEX_PREFIX_POSITION_IN_ALIAS = 1;
|
|
81
81
|
const NAME_SEPARATOR = ".";
|
|
82
|
+
/**
|
|
83
|
+
* Marks an indice the core must leave alone: neither given a missing alias
|
|
84
|
+
* (`generateMissingAliases`) nor counted in the storage stats. Set by tools
|
|
85
|
+
* building a collection's next indice beside it (e.g. a mapping migration).
|
|
86
|
+
* It sits outside the `@&` / `@%` alias convention, so every alias reader
|
|
87
|
+
* already ignores it.
|
|
88
|
+
*/
|
|
89
|
+
const UNMANAGED_INDICE_ALIAS = "kuzzle-unmanaged";
|
|
82
90
|
const FORBIDDEN_CHARS = `\\/*?"<>| \t\r\n,+#:${NAME_SEPARATOR}${PUBLIC_PREFIX}${PRIVATE_PREFIX}`;
|
|
83
91
|
const DYNAMIC_PROPERTY_VALUES = ["true", "false", "strict"];
|
|
84
92
|
// used to check whether we need to wait for ES to initialize or not
|
|
@@ -211,13 +219,15 @@ class ES8 {
|
|
|
211
219
|
metric: ["docs", "store"],
|
|
212
220
|
};
|
|
213
221
|
const stats = await this._client.indices.stats(esRequest);
|
|
222
|
+
const unmanaged = await this._listUnmanagedIndices();
|
|
214
223
|
const indexes = {};
|
|
215
224
|
let size = 0;
|
|
216
225
|
for (const [indice, indiceInfo] of Object.entries(stats.indices ?? {})) {
|
|
217
226
|
const infos = indiceInfo;
|
|
218
|
-
// Ignore non-Kuzzle indices
|
|
219
|
-
if (!indice.startsWith(PRIVATE_PREFIX) &&
|
|
220
|
-
!indice.startsWith(PUBLIC_PREFIX))
|
|
227
|
+
// Ignore non-Kuzzle indices, and the ones Kuzzle must leave alone
|
|
228
|
+
if ((!indice.startsWith(PRIVATE_PREFIX) &&
|
|
229
|
+
!indice.startsWith(PUBLIC_PREFIX)) ||
|
|
230
|
+
unmanaged.has(indice)) {
|
|
221
231
|
continue;
|
|
222
232
|
}
|
|
223
233
|
const aliases = await this._getAliasFromIndice(indice);
|
|
@@ -2547,6 +2557,15 @@ class ES8 {
|
|
|
2547
2557
|
index: index ?? "",
|
|
2548
2558
|
}));
|
|
2549
2559
|
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Returns the indices marked with `UNMANAGED_INDICE_ALIAS`.
|
|
2562
|
+
*/
|
|
2563
|
+
async _listUnmanagedIndices() {
|
|
2564
|
+
const body = await this._catAliases();
|
|
2565
|
+
return new Set(body
|
|
2566
|
+
.filter(({ alias }) => alias === UNMANAGED_INDICE_ALIAS)
|
|
2567
|
+
.map(({ index }) => index));
|
|
2568
|
+
}
|
|
2550
2569
|
/**
|
|
2551
2570
|
* Check for each indice whether it has an alias or not.
|
|
2552
2571
|
* When the latter is missing, create one based on the indice name.
|
|
@@ -2559,7 +2578,9 @@ class ES8 {
|
|
|
2559
2578
|
const body = await this._client.cat.indices({ format: "json" });
|
|
2560
2579
|
const indices = body.flatMap(({ index: indice }) => indice === undefined ? [] : [indice]);
|
|
2561
2580
|
const aliases = await this.listAliases();
|
|
2581
|
+
const unmanaged = await this._listUnmanagedIndices();
|
|
2562
2582
|
const indicesWithoutAlias = indices.filter((indice) => indice[INDEX_PREFIX_POSITION_IN_INDICE] === this._indexPrefix &&
|
|
2583
|
+
!unmanaged.has(indice) &&
|
|
2563
2584
|
!aliases.some((alias) => alias.indice === indice));
|
|
2564
2585
|
const esRequest = {
|
|
2565
2586
|
body: { actions: [] },
|
|
@@ -3,6 +3,12 @@ import type { PolicyRestrictions, OptimizedPolicyRestrictions } from "./PolicyRe
|
|
|
3
3
|
* Policy definition for a role
|
|
4
4
|
* and its restrictions
|
|
5
5
|
*
|
|
6
|
+
* The same policy as the API contract's `ProfilePolicy`, typed differently on
|
|
7
|
+
* purpose: `ProfilePolicy.restrictedTo` is a one-element tuple and leaves
|
|
8
|
+
* `collections` optional; here it is a list of any length and
|
|
9
|
+
* `PolicyRestrictions.collections` is required. Kept apart, since aligning
|
|
10
|
+
* either one would stop code written against it from compiling.
|
|
11
|
+
*
|
|
6
12
|
* @example
|
|
7
13
|
* {
|
|
8
14
|
* "roleId": "admin",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { RoleRightsDefinition } from "kuzzle-types";
|
|
1
2
|
/**
|
|
2
|
-
* A role definition
|
|
3
|
+
* A role definition: its rights, under `controllers`, in the shape of the API
|
|
4
|
+
* contract's `RoleRightsDefinition`
|
|
3
5
|
*
|
|
4
6
|
* @see https://docs.kuzzle.io/core/2/guides/main-concepts/permissions/#roles
|
|
5
7
|
*
|
|
@@ -17,11 +19,5 @@
|
|
|
17
19
|
* }
|
|
18
20
|
*/
|
|
19
21
|
export type RoleDefinition = {
|
|
20
|
-
controllers:
|
|
21
|
-
[controllerName: string]: {
|
|
22
|
-
actions: {
|
|
23
|
-
[actionName: string]: boolean;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
};
|
|
22
|
+
controllers: RoleRightsDefinition;
|
|
27
23
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ByteSize, ClusterNodesStats } from "sdk-es7/api/types";
|
|
2
2
|
import type { JSONObject } from "../../JSONObject";
|
|
3
|
+
export type { KRequestBody, KuzzleInfo } from "../KuzzleInfo";
|
|
3
4
|
export type InfoResult = {
|
|
4
5
|
type: string;
|
|
5
6
|
version: string;
|
|
@@ -8,27 +9,6 @@ export type InfoResult = {
|
|
|
8
9
|
spaceUsed?: ByteSize;
|
|
9
10
|
nodes?: ClusterNodesStats;
|
|
10
11
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Kuzzle's own metadata, stored alongside every document it writes.
|
|
13
|
-
*
|
|
14
|
-
* `author` and `updater` are kuids, and `getKuid` answers `null` for an
|
|
15
|
-
* anonymous write — so both are nullable, as is `updatedAt` on a document that
|
|
16
|
-
* has only ever been created.
|
|
17
|
-
*/
|
|
18
|
-
export type KuzzleInfo = {
|
|
19
|
-
author: string | null;
|
|
20
|
-
createdAt: number;
|
|
21
|
-
updatedAt: number | null;
|
|
22
|
-
updater: string | null;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* `_kuzzle_info` is `Partial` because a partial update writes only the two
|
|
26
|
-
* fields it owns (`updatedAt`, `updater`) and leaves the creation pair to the
|
|
27
|
-
* document already in the index.
|
|
28
|
-
*/
|
|
29
|
-
export type KRequestBody<T> = T & {
|
|
30
|
-
_kuzzle_info?: Partial<KuzzleInfo>;
|
|
31
|
-
};
|
|
32
12
|
export type { JSONObject } from "../../JSONObject";
|
|
33
13
|
export type KImportError = {
|
|
34
14
|
_id?: string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { estypes } from "sdk-es8";
|
|
2
2
|
import type { JSONObject } from "../../JSONObject";
|
|
3
|
+
export type { KRequestBody, KuzzleInfo } from "../KuzzleInfo";
|
|
3
4
|
export type InfoResult = {
|
|
4
5
|
type: string;
|
|
5
6
|
version: string;
|
|
@@ -30,27 +31,6 @@ export type KStats = {
|
|
|
30
31
|
indexes: KStatsIndex[];
|
|
31
32
|
size: estypes.ByteSize;
|
|
32
33
|
};
|
|
33
|
-
/**
|
|
34
|
-
* Kuzzle's own metadata, stored alongside every document it writes.
|
|
35
|
-
*
|
|
36
|
-
* `author` and `updater` are kuids, and `getKuid` answers `null` for an
|
|
37
|
-
* anonymous write — so both are nullable, as is `updatedAt` on a document that
|
|
38
|
-
* has only ever been created.
|
|
39
|
-
*/
|
|
40
|
-
export type KuzzleInfo = {
|
|
41
|
-
author: string | null;
|
|
42
|
-
createdAt: number;
|
|
43
|
-
updatedAt: number | null;
|
|
44
|
-
updater: string | null;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* `_kuzzle_info` is `Partial` because a partial update writes only the two
|
|
48
|
-
* fields it owns (`updatedAt`, `updater`) and leaves the creation pair to the
|
|
49
|
-
* document already in the index.
|
|
50
|
-
*/
|
|
51
|
-
export type KRequestBody<T> = T & {
|
|
52
|
-
_kuzzle_info?: Partial<KuzzleInfo>;
|
|
53
|
-
};
|
|
54
34
|
export type { JSONObject } from "../../JSONObject";
|
|
55
35
|
export type KImportError = {
|
|
56
36
|
_id?: string | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kuzzle's own metadata, stored alongside every document it writes.
|
|
3
|
+
*
|
|
4
|
+
* `author` and `updater` are kuids, and `getKuid` answers `null` for an
|
|
5
|
+
* anonymous write — so both are nullable, as is `updatedAt` on a document that
|
|
6
|
+
* has only ever been created.
|
|
7
|
+
*
|
|
8
|
+
* Wider than the API contract's `KDocumentKuzzleInfo`, whose `author` is a
|
|
9
|
+
* `string`: that type describes what a client is handed, this one what the
|
|
10
|
+
* storage layer may write. Kept apart on purpose.
|
|
11
|
+
*/
|
|
12
|
+
export type KuzzleInfo = {
|
|
13
|
+
author: string | null;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number | null;
|
|
16
|
+
updater: string | null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `_kuzzle_info` is `Partial` because a partial update writes only the two
|
|
20
|
+
* fields it owns (`updatedAt`, `updater`) and leaves the creation pair to the
|
|
21
|
+
* document already in the index.
|
|
22
|
+
*/
|
|
23
|
+
export type KRequestBody<T> = T & {
|
|
24
|
+
_kuzzle_info?: Partial<KuzzleInfo>;
|
|
25
|
+
};
|
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.
|
|
4
|
+
"version": "2.59.0-beta.1",
|
|
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",
|
package/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.
|
|
4
|
+
"version": "2.59.0-beta.1",
|
|
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",
|