@iobroker/db-objects-redis 7.2.3 → 8.0.0-alpha.1-20260921-e5941ca8a

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 (57) hide show
  1. package/build/cjs/index.d.ts +1 -1
  2. package/build/cjs/index.js +5 -3
  3. package/build/cjs/index.js.map +2 -2
  4. package/build/cjs/lib/objects/constants.js.map +2 -2
  5. package/build/cjs/lib/objects/interview.js +2 -0
  6. package/build/cjs/lib/objects/interview.js.map +1 -1
  7. package/build/cjs/lib/objects/lua-v3/custom.lua +23 -1
  8. package/build/cjs/lib/objects/lua-v3/filter.lua +23 -1
  9. package/build/cjs/lib/objects/lua-v3/programs.lua +23 -1
  10. package/build/cjs/lib/objects/lua-v3/script.lua +23 -1
  11. package/build/cjs/lib/objects/lua-v3/variables.lua +23 -1
  12. package/build/cjs/lib/objects/lua-v4/custom.lua +23 -1
  13. package/build/cjs/lib/objects/lua-v4/filter.lua +23 -1
  14. package/build/cjs/lib/objects/lua-v4/programs.lua +23 -1
  15. package/build/cjs/lib/objects/lua-v4/script.lua +23 -1
  16. package/build/cjs/lib/objects/lua-v4/variables.lua +23 -1
  17. package/build/cjs/lib/objects/lua-v4-no-sets/custom.lua +23 -1
  18. package/build/cjs/lib/objects/lua-v4-no-sets/filter.lua +23 -1
  19. package/build/cjs/lib/objects/lua-v4-no-sets/programs.lua +23 -1
  20. package/build/cjs/lib/objects/lua-v4-no-sets/script.lua +23 -1
  21. package/build/cjs/lib/objects/lua-v4-no-sets/variables.lua +23 -1
  22. package/build/cjs/lib/objects/objectsInRedisClient.d.ts +961 -125
  23. package/build/cjs/lib/objects/objectsInRedisClient.js +1092 -776
  24. package/build/cjs/lib/objects/objectsInRedisClient.js.map +3 -3
  25. package/build/cjs/lib/objects/objectsUtils.d.ts +139 -17
  26. package/build/cjs/lib/objects/objectsUtils.js +212 -192
  27. package/build/cjs/lib/objects/objectsUtils.js.map +3 -3
  28. package/build/esm/index.d.ts +1 -1
  29. package/build/esm/index.d.ts.map +1 -1
  30. package/build/esm/index.js.map +1 -1
  31. package/build/esm/lib/objects/constants.d.ts.map +1 -1
  32. package/build/esm/lib/objects/constants.js.map +1 -1
  33. package/build/esm/lib/objects/lua-v3/custom.lua +23 -1
  34. package/build/esm/lib/objects/lua-v3/filter.lua +23 -1
  35. package/build/esm/lib/objects/lua-v3/programs.lua +23 -1
  36. package/build/esm/lib/objects/lua-v3/script.lua +23 -1
  37. package/build/esm/lib/objects/lua-v3/variables.lua +23 -1
  38. package/build/esm/lib/objects/lua-v4/custom.lua +23 -1
  39. package/build/esm/lib/objects/lua-v4/filter.lua +23 -1
  40. package/build/esm/lib/objects/lua-v4/programs.lua +23 -1
  41. package/build/esm/lib/objects/lua-v4/script.lua +23 -1
  42. package/build/esm/lib/objects/lua-v4/variables.lua +23 -1
  43. package/build/esm/lib/objects/lua-v4-no-sets/custom.lua +23 -1
  44. package/build/esm/lib/objects/lua-v4-no-sets/filter.lua +23 -1
  45. package/build/esm/lib/objects/lua-v4-no-sets/programs.lua +23 -1
  46. package/build/esm/lib/objects/lua-v4-no-sets/script.lua +23 -1
  47. package/build/esm/lib/objects/lua-v4-no-sets/variables.lua +23 -1
  48. package/build/esm/lib/objects/objectsInRedisClient.d.ts +961 -125
  49. package/build/esm/lib/objects/objectsInRedisClient.d.ts.map +1 -1
  50. package/build/esm/lib/objects/objectsInRedisClient.js +880 -571
  51. package/build/esm/lib/objects/objectsInRedisClient.js.map +1 -1
  52. package/build/esm/lib/objects/objectsUtils.d.ts +139 -17
  53. package/build/esm/lib/objects/objectsUtils.d.ts.map +1 -1
  54. package/build/esm/lib/objects/objectsUtils.js +273 -221
  55. package/build/esm/lib/objects/objectsUtils.js.map +1 -1
  56. package/build/tsconfig.build.tsbuildinfo +1 -1
  57. package/package.json +9 -9
@@ -2,25 +2,29 @@
2
2
  * Object DB in REDIS - Client
3
3
  *
4
4
  * MIT License
5
- * Written by bluefox <dogafox@gmail.com>, 2014-2024
5
+ * Written by bluefox <dogafox@gmail.com>, 2014-2026
6
6
  *
7
7
  */
8
- // @ts-expect-error no ts module
9
- import extend from 'node.extend';
10
- import Redis from 'ioredis';
11
- import { tools } from '@iobroker/db-base';
12
8
  import fs from 'node:fs';
13
9
  import path from 'node:path';
14
10
  import crypto from 'node:crypto';
15
11
  import { isDeepStrictEqual } from 'node:util';
12
+ import * as url from 'node:url';
16
13
  import deepClone from 'deep-clone';
17
- import * as utils from '../../lib/objects/objectsUtils.js';
18
14
  import semver from 'semver';
15
+ // @ts-expect-error no ts module
16
+ import extend from 'node.extend';
17
+ import Redis from 'ioredis';
18
+ import { tools } from '@iobroker/db-base';
19
+ import { checkObjectRightsAsync, } from '../../lib/objects/objectsUtils.js';
20
+ import * as utils from '../../lib/objects/objectsUtils.js';
19
21
  import * as CONSTS from '../../lib/objects/constants.js';
20
- import * as url from 'node:url';
21
22
  // eslint-disable-next-line unicorn/prefer-module
22
23
  const thisDir = url.fileURLToPath(new URL('.', import.meta.url || `file://${__filename}`));
23
24
  const ERRORS = CONSTS.ERRORS;
25
+ /**
26
+ * Client for the objects database backed by Redis (or the in-memory redis-protocol server)
27
+ */
24
28
  export class ObjectsInRedisClient {
25
29
  client;
26
30
  fileNamespace;
@@ -47,6 +51,9 @@ export class ObjectsInRedisClient {
47
51
  noLegacyMultihost;
48
52
  userSubscriptions;
49
53
  systemSubscriptions;
54
+ /**
55
+ * @param settings Settings for the objects client including connection and namespaces
56
+ */
50
57
  constructor(settings) {
51
58
  this.settings = settings || {};
52
59
  this.redisNamespace = `${this.settings.redisNamespace || this.settings.connection?.redisNamespace || 'cfg'}.`;
@@ -107,6 +114,9 @@ export class ObjectsInRedisClient {
107
114
  throw new Error(`This host does not support protocol version "${protoVersion}"`);
108
115
  }
109
116
  }
117
+ /**
118
+ * Connect to the objects database and set up the change and file subscriptions
119
+ */
110
120
  connectDb() {
111
121
  this.settings.connection = this.settings.connection || {};
112
122
  const onChange = this.settings.change; // on change handler
@@ -135,7 +145,7 @@ export class ObjectsInRedisClient {
135
145
  if (ready && reconnectCount >= retry_max_count) {
136
146
  return new Error('Stop trying to reconnect');
137
147
  }
138
- // A function that receives an options object as parameter including the retry attempt,
148
+ // A function that receives an options object as a parameter including the retry attempt,
139
149
  // the total_retry_time indicating how much time passed since the last time connected,
140
150
  // the error why the connection was lost and the number of times_connected in total.
141
151
  // If you return a number from this function, the retry will happen exactly after that
@@ -161,7 +171,7 @@ export class ObjectsInRedisClient {
161
171
  const defaultPort = Array.isArray(configuredPort) ? null : configuredPort;
162
172
  this.settings.connection.options.sentinels = this.settings.connection.host.map((redisNode, idx) => ({
163
173
  host: redisNode,
164
- // @ts-expect-error ts does not get that if defPort is null we have an array
174
+ // @ts-expect-error ts does not get that if defPort is null, we have an array
165
175
  port: defaultPort === null ? configuredPort[idx] : defaultPort,
166
176
  }));
167
177
  this.settings.connection.options.name = this.settings.connection.sentinelName
@@ -177,11 +187,11 @@ export class ObjectsInRedisClient {
177
187
  this.settings.connection.options.db = this.settings.connection.options.db || 0;
178
188
  this.settings.connection.options.family = this.settings.connection.options.family || 0;
179
189
  this.settings.connection.options.password =
180
- this.settings.connection.options.auth_pass || this.settings.connection.pass || null;
181
- this.settings.connection.options.autoResubscribe = false; // We do our own resubscribe because other sometimes not work
190
+ this.settings.connection.options.auth_pass || this.settings.connection.pass || undefined;
191
+ this.settings.connection.options.autoResubscribe = false; // We do our own re-subscribing because other sometimes not work
182
192
  // REDIS does not allow whitespaces, we have some because of pid
183
193
  this.settings.connection.options.connectionName = this.namespace.replace(/\s/g, '');
184
- this.client = new Redis(this.settings.connection.options);
194
+ this.client = new Redis.Redis(this.settings.connection.options);
185
195
  this.client.on('error', error => {
186
196
  if (this.settings.connection.enhancedLogging) {
187
197
  this.log.silly(`${this.namespace} Redis ERROR Objects: (${this.stop}) ${error.message} / ${error.stack}`);
@@ -236,7 +246,7 @@ export class ObjectsInRedisClient {
236
246
  this.log.silly(`${this.namespace} Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`);
237
247
  }
238
248
  if (reconnectCounter > 2) {
239
- // fallback logic for nodejs <10
249
+ // fallback logic for Node.js <10
240
250
  this.log.error(`${this.namespace} The DB port ${this.settings.connection.options.port} is occupied by something that is not a Redis protocol server. Please check other software running on this port or, if you use iobroker, make sure to update to js-controller 2.0 or higher!`);
241
251
  return;
242
252
  }
@@ -263,7 +273,7 @@ export class ObjectsInRedisClient {
263
273
  if (!this.subSystem && typeof onChange === 'function') {
264
274
  initCounter++;
265
275
  this.log.debug(`${this.namespace} Objects create System PubSub Client`);
266
- this.subSystem = new Redis(this.settings.connection.options);
276
+ this.subSystem = new Redis.Redis(this.settings.connection.options);
267
277
  if (typeof this.settings.primaryHostLost === 'function') {
268
278
  try {
269
279
  // enable Expiry/Evicted events in server - same as states (could be same db)
@@ -300,7 +310,7 @@ export class ObjectsInRedisClient {
300
310
  if (newUseSets !== this.useSets) {
301
311
  this.log.info(`${this.namespace} Sets ${newUseSets ? 'activated' : 'deactivated'}: restarting ...`);
302
312
  this.useSets = newUseSets;
303
- // luas are no longer up to date, lets restart
313
+ // luas are no longer up to date, let's restart
304
314
  if (typeof this.settings.disconnected === 'function') {
305
315
  this.settings.disconnected();
306
316
  }
@@ -318,7 +328,7 @@ export class ObjectsInRedisClient {
318
328
  !isDeepStrictEqual(obj.common.defaultNewAcl, this.defaultNewAcl)) {
319
329
  this.defaultNewAcl = deepClone(obj.common.defaultNewAcl);
320
330
  if (this.settings.controller) {
321
- this.setDefaultAcl(this.defaultNewAcl);
331
+ this.setDefaultAcl(this.defaultNewAcl).catch(e => this.log.error(`${this.namespace} Cannot set default ACL: ${e.message}`));
322
332
  }
323
333
  }
324
334
  onChange(id, obj);
@@ -357,7 +367,7 @@ export class ObjectsInRedisClient {
357
367
  if (this.settings.connection.enhancedLogging) {
358
368
  this.subSystem.on('connect', () => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event connect (stop=${this.stop})`));
359
369
  this.subSystem.on('close', () => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event close (stop=${this.stop})`));
360
- this.subSystem.on('reconnecting', reconnectCounter => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`));
370
+ this.subSystem.on('reconnecting', (reconnectCounter) => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`));
361
371
  }
362
372
  this.subSystem.on('ready', async () => {
363
373
  if (--initCounter < 1) {
@@ -372,7 +382,7 @@ export class ObjectsInRedisClient {
372
382
  }
373
383
  ready = true;
374
384
  }
375
- // subscribe on system.config anytime because also adapters need stuff like defaultNewAcl (especially admin)
385
+ // subscribe to system.config anytime because also adapters need stuff like defaultNewAcl (especially admin)
376
386
  try {
377
387
  if (this.subSystem) {
378
388
  await this.subSystem.psubscribe(`${this.objNamespace}system.config`);
@@ -405,7 +415,7 @@ export class ObjectsInRedisClient {
405
415
  if (!this.sub && (typeof onChangeUser === 'function' || typeof onChangeFileUser === 'function')) {
406
416
  initCounter++;
407
417
  this.log.debug(`${this.namespace} Objects create User PubSub Client`);
408
- this.sub = new Redis(this.settings.connection.options);
418
+ this.sub = new Redis.Redis(this.settings.connection.options);
409
419
  this.sub.on('pmessage', (pattern, channel, message) => {
410
420
  setImmediate(() => {
411
421
  this.log.silly(`${this.namespace} Objects user redis pmessage ${pattern}/${channel}:${message}`);
@@ -465,11 +475,11 @@ export class ObjectsInRedisClient {
465
475
  if (this.settings.connection.enhancedLogging) {
466
476
  this.sub.on('connect', () => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event connect (stop=${this.stop})`));
467
477
  this.sub.on('close', () => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event close (stop=${this.stop})`));
468
- this.sub.on('reconnecting', reconnectCounter => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`));
478
+ this.sub.on('reconnecting', (reconnectCounter) => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`));
469
479
  }
470
480
  this.sub.on('ready', async () => {
471
481
  if (!this.sub) {
472
- // client gone while ready emitted, can maybe not happen but ts is happy
482
+ // client gone while ready emitted, can maybe not happen, but ts is happy
473
483
  return;
474
484
  }
475
485
  if (--initCounter < 1) {
@@ -502,7 +512,7 @@ export class ObjectsInRedisClient {
502
512
  this.useSets = !!parseInt((await this.client.get(`${this.metaNamespace}objects.features.useSets`)) || '0');
503
513
  }
504
514
  catch (e) {
505
- // if unsupported we have a legacy host
515
+ // if unsupported, we have a legacy host
506
516
  if (!e.message.includes('UNSUPPORTED')) {
507
517
  this.log.error(`${this.namespace} Cannot determine Set feature status: ${e.message}`);
508
518
  return;
@@ -516,13 +526,13 @@ export class ObjectsInRedisClient {
516
526
  this.log.error(`${this.namespace} ${e.message}`);
517
527
  throw new Error('Objects DB is not allowed to start in the current Multihost environment');
518
528
  }
519
- // for controller v4 we have to check if we can use the new lua scripts and set logic
529
+ // for controller v4 we have to check if we can use the new Lua scripts and set logic
520
530
  // TODO: remove this backward shim if controller v4.0 is old enough
521
531
  let keys = await this._getKeysViaScan(`${this.objNamespace}system.host.*`);
522
532
  // filter out obvious non-host objects
523
533
  const hostRegex = new RegExp(`^${this.objNamespace.replace(/\./g, '\\.')}system\\.host\\.[^.]+$`);
524
534
  keys = keys.filter(id => hostRegex.test(id));
525
- /** if false we have a host smaller 4 (no proto version for this existing) */
535
+ /** if false, we have a host smaller 4 (no proto version for this existing) */
526
536
  this.noLegacyMultihost = true;
527
537
  try {
528
538
  if (keys.length) {
@@ -590,6 +600,9 @@ export class ObjectsInRedisClient {
590
600
  }
591
601
  });
592
602
  }
603
+ /**
604
+ * Get the current status of the database
605
+ */
593
606
  getStatus() {
594
607
  return { type: 'redis', server: false };
595
608
  }
@@ -597,7 +610,7 @@ export class ObjectsInRedisClient {
597
610
  * Checks if given ID is a meta-object, else throws error
598
611
  *
599
612
  * @param id to check
600
- * @throws Error if id is invalid
613
+ * @throws {Error} if id is invalid
601
614
  */
602
615
  async validateMetaObject(id) {
603
616
  if (this.existingMetaObjects[id] === undefined) {
@@ -615,6 +628,11 @@ export class ObjectsInRedisClient {
615
628
  throw new Error(`${id} is not an object of type "meta"`);
616
629
  }
617
630
  }
631
+ /**
632
+ * Normalize a file name by collapsing slashes and backslashes into a single forward slash
633
+ *
634
+ * @param name The file name to normalize
635
+ */
618
636
  normalizeFilename(name) {
619
637
  return name ? name.replace(/[/\\]+/g, '/') : name;
620
638
  }
@@ -622,7 +640,7 @@ export class ObjectsInRedisClient {
622
640
  /**
623
641
  * Sets a buffer to the Redis DB
624
642
  *
625
- * @param id id of the file
643
+ * @param id ID of the file
626
644
  * @param data content, if string is passed it will be converted to a Buffer
627
645
  */
628
646
  async _setBinaryState(id, data) {
@@ -636,7 +654,7 @@ export class ObjectsInRedisClient {
636
654
  await this.client.publish(id, data.byteLength.toString(10));
637
655
  }
638
656
  /**
639
- * get buffer of given id from redis
657
+ * Get a buffer of a given ID from redis
640
658
  *
641
659
  * @param id - id of the data with namespace prefix
642
660
  */
@@ -647,7 +665,7 @@ export class ObjectsInRedisClient {
647
665
  return this.client.getBuffer(id);
648
666
  }
649
667
  /**
650
- * deletes binary state of given id from redis db
668
+ * deletes binary state of a given ID from redis db
651
669
  *
652
670
  * @param id - id to delete, with namespace prefix
653
671
  */
@@ -660,6 +678,13 @@ export class ObjectsInRedisClient {
660
678
  await this.client.publish(id, 'null'); // inform about deletion
661
679
  }
662
680
  }
681
+ /**
682
+ * Build the internal redis key for a file
683
+ *
684
+ * @param id The ID of the object owning the file
685
+ * @param name The file name
686
+ * @param isMeta Whether to return the key of the meta-entry (true) or the data entry (false)
687
+ */
663
688
  getFileId(id, name, isMeta) {
664
689
  name = this.normalizeFilename(name);
665
690
  // e.g. ekey.admin and admin/ekey.png
@@ -686,42 +711,66 @@ export class ObjectsInRedisClient {
686
711
  name = normalized.name;
687
712
  return `${this.fileNamespace + id}$%$${name}${isMeta !== undefined ? (isMeta ? '$%$meta' : '$%$data') : ''}`;
688
713
  }
689
- async checkFile(id, name, options, flag, callback) {
714
+ /**
715
+ * Check whether the current options have the required rights on a file
716
+ *
717
+ * @param id The id of the object owning the file
718
+ * @param name The file name
719
+ * @param userContext The resolved user context used for permission checks
720
+ * @param flag The access flag(s) to check for
721
+ */
722
+ async checkFileAsync(id, name, userContext, flag) {
690
723
  // read file settings from redis
691
- const fileId = this.getFileId(id, name, true);
724
+ const fileId = this.getFileId(id, name ?? '', true);
692
725
  if (!fileId) {
693
- const fileOptions = { notExists: true };
694
- if (utils.checkFile(fileOptions, options, flag, this.defaultNewAcl)) {
695
- return tools.maybeCallback(callback, false, options, fileOptions); // NO error
726
+ if (!utils.checkFile({ notExists: true }, userContext, flag, this.defaultNewAcl)) {
727
+ return null;
696
728
  }
697
- return tools.maybeCallback(callback, true, options); // error
729
+ return { notExists: true };
698
730
  }
699
731
  if (!this.client) {
700
- // @ts-expect-error TODO: not in specs, better just maybe cb check false?
701
- return tools.maybeCallbackWithRedisError(callback, ERRORS.ERROR_DB_CLOSED, options);
732
+ throw new Error(ERRORS.ERROR_DB_CLOSED);
702
733
  }
703
- let fileOptions;
734
+ let fileOptionsStr;
704
735
  try {
705
- fileOptions = await this.client.get(fileId);
736
+ fileOptionsStr = await this.client.get(fileId);
706
737
  }
707
738
  catch {
708
739
  // ignore
709
740
  }
710
- fileOptions = fileOptions || '{"notExists": true}';
741
+ fileOptionsStr ||= '{"notExists": true}';
742
+ let fileOptions;
711
743
  try {
712
- fileOptions = JSON.parse(fileOptions);
744
+ fileOptions = JSON.parse(fileOptionsStr);
713
745
  }
714
746
  catch {
715
- this.log.error(`${this.namespace} Cannot parse JSON ${id}: ${fileOptions}`);
747
+ this.log.error(`${this.namespace} Cannot parse JSON ${id}: ${fileOptionsStr}`);
716
748
  fileOptions = { notExists: true };
717
749
  }
718
- if (utils.checkFile(fileOptions, options, flag, this.defaultNewAcl)) {
719
- return tools.maybeCallback(callback, false, options, fileOptions); // NO error
750
+ if (!utils.checkFile(fileOptions, userContext, flag, this.defaultNewAcl)) {
751
+ return null;
720
752
  }
721
- return tools.maybeCallback(callback, true, options); // error
753
+ return fileOptions;
722
754
  }
755
+ /**
756
+ * Check whether the current user is allowed to access a file
757
+ *
758
+ * @param id The id of the object owning the file
759
+ * @param name The file name, or null for the whole namespace
760
+ * @param options The current request options including the user
761
+ * @param flag The access flag(s) to check for
762
+ * @param callback Called with the effective options once the rights have been checked
763
+ */
723
764
  checkFileRights(id, name, options, flag, callback) {
724
- return utils.checkFileRights(this, id, name, options, flag, callback);
765
+ try {
766
+ utils
767
+ .checkFileRightsAsync(this, id, name, options, flag)
768
+ .then(result => callback(null, result.fileOptions, result.userContext))
769
+ .catch(err => callback(err));
770
+ }
771
+ catch (error) {
772
+ callback(error);
773
+ }
725
774
  }
726
775
  async _setDefaultAcl(ids, defaultAcl) {
727
776
  for (const id of ids) {
@@ -737,6 +786,11 @@ export class ObjectsInRedisClient {
737
786
  }
738
787
  }
739
788
  }
789
+ /**
790
+ * Set the default ACL applied to new objects and apply it to all existing objects without an ACL
791
+ *
792
+ * @param defaultNewAcl The default ACL to use, or null to use the built-in default
793
+ */
740
794
  async setDefaultAcl(defaultNewAcl) {
741
795
  this.defaultNewAcl = defaultNewAcl || {
742
796
  owner: CONSTS.SYSTEM_ADMIN_USER,
@@ -756,17 +810,25 @@ export class ObjectsInRedisClient {
756
810
  this.log.error(`${this.namespace} Could not update default acl: ${e.message}`);
757
811
  }
758
812
  }
813
+ /**
814
+ * Determine the groups and effective ACL of the given user
815
+ *
816
+ * @param user The id of the user to look up
817
+ * @param callback Called with the user, its groups, and the effective ACL
818
+ */
759
819
  getUserGroup(user, callback) {
760
820
  return utils.getUserGroup(this, user, (error, user, userGroups, userAcl) => {
761
821
  if (error) {
762
822
  this.log.error(`${this.namespace} ${error.stack}`);
763
823
  }
764
- return tools.maybeCallback(callback, user, userGroups, userAcl);
824
+ if (typeof callback === 'function') {
825
+ callback(user, userGroups, userAcl);
826
+ }
765
827
  });
766
828
  }
767
829
  async _writeFile(id, name, data, options, callback, meta) {
768
830
  const matchedExtension = name.match(/\.[^.]+$/);
769
- const ext = matchedExtension ? matchedExtension[0] : '';
831
+ const ext = matchedExtension?.[0] || '';
770
832
  const isTextData = typeof data === 'string';
771
833
  const { mimeType, isBinary } = utils.getMimeType(ext, isTextData);
772
834
  const metaID = this.getFileId(id, name, true);
@@ -788,33 +850,24 @@ export class ObjectsInRedisClient {
788
850
  }
789
851
  }
790
852
  else {
791
- if (!meta) {
792
- meta = { createdAt: Date.now() };
793
- }
794
- if (!meta.acl) {
795
- meta.acl = {
796
- owner: options.user || (this.defaultNewAcl && this.defaultNewAcl.owner) || CONSTS.SYSTEM_ADMIN_USER,
797
- ownerGroup: options.group ||
798
- (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
799
- CONSTS.SYSTEM_ADMIN_GROUP,
800
- permissions: options.mode || (this.defaultNewAcl && this.defaultNewAcl.file) || 0x644,
801
- };
802
- }
853
+ meta ||= { createdAt: Date.now() };
854
+ meta.acl ||= {
855
+ owner: options.user || this.defaultNewAcl?.owner || CONSTS.SYSTEM_ADMIN_USER,
856
+ ownerGroup: options.group || this.defaultNewAcl?.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP,
857
+ permissions: options.mode || this.defaultNewAcl?.file || 0x644,
858
+ };
803
859
  meta.stats = {
804
- size: data ? data.length : 0,
860
+ size: data?.length || 0,
805
861
  };
806
862
  if (Object.prototype.hasOwnProperty.call(meta, 'notExists')) {
807
863
  delete meta.notExists;
808
864
  }
809
865
  meta.mimeType = options.mimeType || mimeType;
810
866
  meta.binary = isBinary;
811
- meta.acl.ownerGroup =
812
- meta.acl.ownerGroup ||
813
- (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
814
- CONSTS.SYSTEM_ADMIN_GROUP;
867
+ meta.acl.ownerGroup ||= this.defaultNewAcl?.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP;
815
868
  meta.modifiedAt = Date.now();
816
869
  try {
817
- await this._setBinaryState(this.getFileId(id, name, false), data);
870
+ await this._setBinaryState(this.getFileId(id, name, false), data || '');
818
871
  await this.client.set(metaID, JSON.stringify(meta));
819
872
  return tools.maybeCallback(callback);
820
873
  }
@@ -823,17 +876,20 @@ export class ObjectsInRedisClient {
823
876
  }
824
877
  }
825
878
  }
879
+ /**
880
+ * Write data into a file of an object
881
+ *
882
+ * @param id The id of the object owning the file
883
+ * @param name The file name
884
+ * @param data The data to write
885
+ * @param options The current request options including the user, or the callback
886
+ * @param callback Called once the file has been written
887
+ */
826
888
  async writeFile(id, name, data, options, callback) {
827
889
  if (typeof options === 'function') {
828
890
  callback = options;
829
891
  options = null;
830
892
  }
831
- if (typeof options === 'string') {
832
- options = { mimeType: options };
833
- }
834
- if (options?.acl) {
835
- options.acl = null;
836
- }
837
893
  if (!callback) {
838
894
  return this.writeFileAsync(id, name, data, options);
839
895
  }
@@ -854,13 +910,21 @@ export class ObjectsInRedisClient {
854
910
  data = null;
855
911
  }
856
912
  // If file yet exists => check the permissions
857
- return this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, options, meta) => {
913
+ return this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, meta) => {
858
914
  if (err) {
859
915
  return tools.maybeCallbackWithError(callback, err);
860
916
  }
861
- return this._writeFile(id, name, data, options, callback, meta);
917
+ return this._writeFile(id, name, data, options || {}, callback, meta);
862
918
  });
863
919
  }
920
+ /**
921
+ * Promise-version of writeFile
922
+ *
923
+ * @param id The ID of the object owning the file
924
+ * @param name The file name
925
+ * @param data The data to write
926
+ * @param options The current request options including the user
927
+ */
864
928
  writeFileAsync(id, name, data, options) {
865
929
  return new Promise((resolve, reject) => this.writeFile(id, name, data, options, err => (err ? reject(err) : resolve())));
866
930
  }
@@ -871,22 +935,26 @@ export class ObjectsInRedisClient {
871
935
  if (!this.client) {
872
936
  throw new Error(ERRORS.ERROR_DB_CLOSED);
873
937
  }
874
- let buffer;
875
- buffer = await this._getBinaryState(this.getFileId(id, name, false));
938
+ let file = await this._getBinaryState(this.getFileId(id, name, false));
876
939
  const mimeType = meta?.mimeType;
877
- if (meta && !meta.binary && buffer) {
878
- buffer = buffer.toString();
940
+ if (meta && !meta.binary && file) {
941
+ file = file.toString();
879
942
  }
880
- return { file: buffer, mimeType: mimeType };
943
+ return { file, mimeType };
881
944
  }
945
+ /**
946
+ * Read a file of an object
947
+ *
948
+ * @param id The ID of the object owning the file
949
+ * @param name The file name
950
+ * @param options The current request options including the user, or the callback
951
+ * @param callback Called with the file content and mime type
952
+ */
882
953
  readFile(id, name, options, callback) {
883
954
  if (typeof options === 'function') {
884
955
  callback = options;
885
956
  options = null;
886
957
  }
887
- if (options?.acl) {
888
- options.acl = null;
889
- }
890
958
  if (!callback) {
891
959
  return new Promise((resolve, reject) => this.readFile(id, name, options, (err, res, mimeType) => err ? reject(err) : resolve({ file: res, mimeType: mimeType })));
892
960
  }
@@ -896,8 +964,8 @@ export class ObjectsInRedisClient {
896
964
  if (name.startsWith('/')) {
897
965
  name = name.substring(1);
898
966
  }
899
- options = options || {};
900
- this.checkFileRights(id, name, options, CONSTS.ACCESS_READ, async (err, options, meta) => {
967
+ options ||= {};
968
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_READ, async (err, meta) => {
901
969
  if (err) {
902
970
  return tools.maybeCallbackWithError(callback, err);
903
971
  }
@@ -911,9 +979,9 @@ export class ObjectsInRedisClient {
911
979
  });
912
980
  }
913
981
  /**
914
- * Check if given object exists
982
+ * Check if a given object exists
915
983
  *
916
- * @param id id of the object
984
+ * @param id ID of the object
917
985
  * @param options optional user context
918
986
  */
919
987
  async objectExists(id, options) {
@@ -921,18 +989,11 @@ export class ObjectsInRedisClient {
921
989
  throw new Error(ERRORS.ERROR_DB_CLOSED);
922
990
  }
923
991
  if (!id || typeof id !== 'string') {
924
- throw new Error(`invalid id ${JSON.stringify(id)}`);
992
+ throw new Error(`invalid ID ${JSON.stringify(id)}`);
925
993
  }
926
994
  try {
927
995
  await new Promise((resolve, reject) => {
928
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_LIST, err => {
929
- if (err) {
930
- reject(err);
931
- }
932
- else {
933
- resolve();
934
- }
935
- });
996
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_LIST, err => err ? reject(err) : resolve());
936
997
  });
937
998
  const exists = await this.client.exists(this.objNamespace + id);
938
999
  return !!exists;
@@ -943,9 +1004,9 @@ export class ObjectsInRedisClient {
943
1004
  }
944
1005
  }
945
1006
  /**
946
- * Check if given file exists
1007
+ * Check if a given file exists
947
1008
  *
948
- * @param id id of the namespace
1009
+ * @param id ID of the namespace
949
1010
  * @param name name of the file
950
1011
  * @param options optional user context
951
1012
  */
@@ -957,16 +1018,7 @@ export class ObjectsInRedisClient {
957
1018
  name = name.substring(1);
958
1019
  }
959
1020
  try {
960
- await new Promise((resolve, reject) => {
961
- this.checkFileRights(id, name, options, CONSTS.ACCESS_READ, err => {
962
- if (err) {
963
- reject(err);
964
- }
965
- else {
966
- resolve();
967
- }
968
- });
969
- });
1021
+ await utils.checkFileRightsAsync(this, id, name, options, CONSTS.ACCESS_READ);
970
1022
  if (!this.client) {
971
1023
  throw new Error(ERRORS.ERROR_DB_CLOSED);
972
1024
  }
@@ -979,41 +1031,46 @@ export class ObjectsInRedisClient {
979
1031
  throw new Error(`Cannot check file existence of "${id}": ${e.message}`);
980
1032
  }
981
1033
  }
982
- async _unlink(id, name, options, meta) {
1034
+ async _unlink(id, name, userContext, meta) {
983
1035
  if (!this.client) {
984
1036
  throw new Error(ERRORS.ERROR_DB_CLOSED);
985
1037
  }
986
- if (meta && meta.notExists) {
987
- return this._rm(id, name, options);
1038
+ if (meta?.notExists) {
1039
+ return this._rm(id, name, userContext);
988
1040
  }
989
1041
  const metaID = this.getFileId(id, name, true);
990
1042
  const dataID = this.getFileId(id, name, false);
991
1043
  await this._delBinaryState(dataID);
992
1044
  await this.client.del(metaID);
993
1045
  }
1046
+ /**
1047
+ * Delete a file or directory of an object
1048
+ *
1049
+ * @param id The id of the object owning the file
1050
+ * @param name The file or directory name to delete
1051
+ * @param options The current request options including the user, or the callback
1052
+ * @param callback Called with the list of removed files
1053
+ */
994
1054
  unlink(id, name, options, callback) {
995
1055
  if (typeof options === 'function') {
996
1056
  callback = options;
997
1057
  options = null;
998
1058
  }
999
- if (options?.acl) {
1000
- options.acl = null;
1001
- }
1002
1059
  if (typeof name !== 'string') {
1003
1060
  name = '';
1004
1061
  }
1005
1062
  if (name.startsWith('/')) {
1006
1063
  name = name.substring(1);
1007
1064
  }
1008
- this.checkFileRights(id, name, options, CONSTS.ACCESS_DELETE, async (err, options, meta) => {
1065
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_DELETE, async (err, meta, userContext) => {
1009
1066
  if (err) {
1010
1067
  return tools.maybeCallbackWithError(callback, err);
1011
1068
  }
1012
- if (!options.acl.file.delete) {
1069
+ if (!userContext.acl.file.delete) {
1013
1070
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1014
1071
  }
1015
1072
  try {
1016
- const files = await this._unlink(id, name, options, meta);
1073
+ const files = await this._unlink(id, name, userContext, meta);
1017
1074
  return tools.maybeCallbackWithError(callback, null, files);
1018
1075
  }
1019
1076
  catch (e) {
@@ -1021,16 +1078,38 @@ export class ObjectsInRedisClient {
1021
1078
  }
1022
1079
  });
1023
1080
  }
1081
+ /**
1082
+ * Promise-version of unlink
1083
+ *
1084
+ * @param id The id of the object owning the file
1085
+ * @param name The file or directory name to delete
1086
+ * @param options The current request options including the user
1087
+ */
1024
1088
  unlinkAsync(id, name, options) {
1025
1089
  return new Promise((resolve, reject) => this.unlink(id, name, options, err => (err ? reject(err) : resolve())));
1026
1090
  }
1091
+ /**
1092
+ * Delete a file of an object (alias for {@link unlink})
1093
+ *
1094
+ * @param id The id of the object owning the file
1095
+ * @param name The file name to delete
1096
+ * @param options The current request options including the user
1097
+ * @param callback Called once the file has been deleted
1098
+ */
1027
1099
  delFile(id, name, options, callback) {
1028
1100
  return this.unlink(id, name, options, callback);
1029
1101
  }
1102
+ /**
1103
+ * Promise-version of delFile
1104
+ *
1105
+ * @param id The id of the object owning the file
1106
+ * @param name The file name to delete
1107
+ * @param options The current request options including the user
1108
+ */
1030
1109
  delFileAsync(id, name, options) {
1031
1110
  return this.unlinkAsync(id, name, options);
1032
1111
  }
1033
- async _readDir(id, name, options, callback) {
1112
+ async _readDir(id, name, userContext, callback) {
1034
1113
  name = this.normalizeFilename(name);
1035
1114
  if (!this.client) {
1036
1115
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
@@ -1120,9 +1199,9 @@ export class ObjectsInRedisClient {
1120
1199
  return tools.maybeCallbackWithRedisError(callback, e);
1121
1200
  }
1122
1201
  const result = [];
1123
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
1124
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
1125
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1202
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
1203
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
1204
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1126
1205
  for (let i = 0; i < keys.length; i++) {
1127
1206
  const file = keys[i].substring(start + baseName.length, keys[i].length - end);
1128
1207
  while (dirs.length && dirs[0] < file) {
@@ -1141,13 +1220,14 @@ export class ObjectsInRedisClient {
1141
1220
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
1142
1221
  continue;
1143
1222
  }
1144
- if (dontCheck || utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
1223
+ if (dontCheck || utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
1145
1224
  if (!obj || obj.virtualFile) {
1146
1225
  // virtual file, ignore
1147
1226
  continue;
1148
1227
  }
1149
- obj.acl = obj.acl || {};
1150
- if (options.user !== CONSTS.SYSTEM_ADMIN_USER && !options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP)) {
1228
+ obj.acl ||= {};
1229
+ if (userContext.user !== CONSTS.SYSTEM_ADMIN_USER &&
1230
+ !userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP)) {
1151
1231
  obj.acl.read = !!(obj.acl.permissions & CONSTS.ACCESS_EVERY_READ);
1152
1232
  obj.acl.write = !!(obj.acl.permissions & CONSTS.ACCESS_EVERY_WRITE);
1153
1233
  }
@@ -1156,8 +1236,8 @@ export class ObjectsInRedisClient {
1156
1236
  obj.acl.write = true;
1157
1237
  }
1158
1238
  result.push({
1159
- file: file,
1160
- stats: obj.stats,
1239
+ file,
1240
+ stats: obj.stats || {},
1161
1241
  isDir: false,
1162
1242
  acl: obj.acl,
1163
1243
  modifiedAt: obj.modifiedAt,
@@ -1174,14 +1254,19 @@ export class ObjectsInRedisClient {
1174
1254
  }
1175
1255
  return tools.maybeCallbackWithError(callback, null, result);
1176
1256
  }
1257
+ /**
1258
+ * List the contents of a directory of an object
1259
+ *
1260
+ * @param id The id of the object owning the files
1261
+ * @param name The directory name to list
1262
+ * @param options The current request options including the user, or the callback
1263
+ * @param callback Called with the directory entries
1264
+ */
1177
1265
  readDir(id, name, options, callback) {
1178
1266
  if (typeof options === 'function') {
1179
1267
  callback = options;
1180
1268
  options = null;
1181
1269
  }
1182
- if (options?.acl) {
1183
- options.acl = null;
1184
- }
1185
1270
  if (typeof name !== 'string') {
1186
1271
  name = '';
1187
1272
  }
@@ -1192,16 +1277,23 @@ export class ObjectsInRedisClient {
1192
1277
  if (name.endsWith('/')) {
1193
1278
  name = name.substring(0, name.length - 1);
1194
1279
  }
1195
- this.checkFileRights(id, name, options, CONSTS.ACCESS_READ, (err, options) => {
1280
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_READ, (err, options, userContext) => {
1196
1281
  if (err) {
1197
1282
  return tools.maybeCallbackWithError(callback, err);
1198
1283
  }
1199
- if (!options.acl.file.list) {
1284
+ if (!userContext.acl.file.list) {
1200
1285
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1201
1286
  }
1202
- this._readDir(id, name, options, callback);
1287
+ return this._readDir(id, name, userContext, callback);
1203
1288
  });
1204
1289
  }
1290
+ /**
1291
+ * Promise-version of readDir
1292
+ *
1293
+ * @param id The id of the object owning the files
1294
+ * @param name The directory name to list
1295
+ * @param options The current request options including the user
1296
+ */
1205
1297
  readDirAsync(id, name, options) {
1206
1298
  return new Promise((resolve, reject) => this.readDir(id, name, options, (err, res) => (err ? reject(err) : resolve(res))));
1207
1299
  }
@@ -1231,7 +1323,7 @@ export class ObjectsInRedisClient {
1231
1323
  }
1232
1324
  return tools.maybeCallback(callback);
1233
1325
  }
1234
- async _rename(id, oldName, newName, options, callback, meta) {
1326
+ async _rename(id, oldName, newName, userContext, callback, meta) {
1235
1327
  const oldMetaID = this.getFileId(id, oldName, true);
1236
1328
  const oldDataID = this.getFileId(id, oldName, false);
1237
1329
  const newMetaID = this.getFileId(id, newName, true);
@@ -1284,9 +1376,9 @@ export class ObjectsInRedisClient {
1284
1376
  return tools.maybeCallbackWithRedisError(callback, e);
1285
1377
  }
1286
1378
  let result;
1287
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
1288
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
1289
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1379
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
1380
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
1381
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1290
1382
  if (!dontCheck) {
1291
1383
  result = [];
1292
1384
  for (let i = 0; i < keys.length; i++) {
@@ -1299,7 +1391,7 @@ export class ObjectsInRedisClient {
1299
1391
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
1300
1392
  continue;
1301
1393
  }
1302
- if (utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
1394
+ if (utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
1303
1395
  result.push(keys[i]);
1304
1396
  }
1305
1397
  }
@@ -1318,14 +1410,20 @@ export class ObjectsInRedisClient {
1318
1410
  return tools.maybeCallbackWithRedisError(callback, e);
1319
1411
  }
1320
1412
  }
1413
+ /**
1414
+ * Rename a file or directory of an object
1415
+ *
1416
+ * @param id The id of the object owning the file
1417
+ * @param oldName The current file or directory name
1418
+ * @param newName The new file or directory name
1419
+ * @param options The current request options including the user, or the callback
1420
+ * @param callback Called once the file has been renamed
1421
+ */
1321
1422
  rename(id, oldName, newName, options, callback) {
1322
1423
  if (typeof options === 'function') {
1323
1424
  callback = options;
1324
1425
  options = null;
1325
1426
  }
1326
- if (options?.acl) {
1327
- options.acl = null;
1328
- }
1329
1427
  if (typeof oldName !== 'string' ||
1330
1428
  !oldName.length ||
1331
1429
  oldName === '/' ||
@@ -1348,20 +1446,29 @@ export class ObjectsInRedisClient {
1348
1446
  if (newName.endsWith('/')) {
1349
1447
  newName = newName.substring(0, newName.length - 1);
1350
1448
  }
1351
- this.checkFileRights(id, oldName, options, CONSTS.ACCESS_WRITE, (err, options, meta) => {
1449
+ this.checkFileRights(id, oldName, options, CONSTS.ACCESS_WRITE, (err, meta, userContext) => {
1352
1450
  if (err) {
1353
1451
  return tools.maybeCallbackWithError(callback, err);
1354
1452
  }
1355
- if (!options.acl.file.write) {
1453
+ if (!userContext.acl.file.write) {
1356
1454
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1357
1455
  }
1358
- this._rename(id, oldName, newName, options, callback, meta);
1456
+ return this._rename(id, oldName, newName, userContext, callback, meta);
1359
1457
  });
1360
1458
  }
1459
+ /**
1460
+ * Promise-version of rename
1461
+ *
1462
+ * @param id The id of the object owning the file
1463
+ * @param oldName The current file or directory name
1464
+ * @param newName The new file or directory name
1465
+ * @param options The current request options including the user
1466
+ * @param options.user The user on whose behalf the operation is performed
1467
+ */
1361
1468
  renameAsync(id, oldName, newName, options) {
1362
1469
  return new Promise((resolve, reject) => this.rename(id, oldName, newName, options, err => (err ? reject(err) : resolve())));
1363
1470
  }
1364
- async _touch(id, name, callback, meta) {
1471
+ async _touch(id, name, meta, callback) {
1365
1472
  const metaID = this.getFileId(id, name, true);
1366
1473
  if (!this.client) {
1367
1474
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
@@ -1378,27 +1485,40 @@ export class ObjectsInRedisClient {
1378
1485
  return tools.maybeCallbackWithRedisError(callback, e);
1379
1486
  }
1380
1487
  }
1488
+ /**
1489
+ * Update the modification time of a file
1490
+ *
1491
+ * @param id The id of the object owning the file
1492
+ * @param name The file name
1493
+ * @param options The current request options including the user, or the callback
1494
+ * @param callback Called once the file has been touched
1495
+ */
1381
1496
  touch(id, name, options, callback) {
1382
1497
  if (typeof options === 'function') {
1383
1498
  callback = options;
1384
1499
  options = null;
1385
1500
  }
1386
- if (options?.acl) {
1387
- options.acl = null;
1388
- }
1389
1501
  if (typeof name !== 'string' || !name.length || name === '/') {
1390
1502
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND);
1391
1503
  }
1392
1504
  if (name.startsWith('/')) {
1393
1505
  name = name.substring(1);
1394
1506
  }
1395
- this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, options, meta) => {
1507
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, meta) => {
1396
1508
  if (err) {
1397
1509
  return tools.maybeCallbackWithError(callback, err);
1398
1510
  }
1399
- return this._touch(id, name, callback, meta);
1511
+ return this._touch(id, name, meta, callback);
1400
1512
  });
1401
1513
  }
1514
+ /**
1515
+ * Promise-version of touch
1516
+ *
1517
+ * @param id The id of the object owning the file
1518
+ * @param name The file name
1519
+ * @param options The current request options including the user
1520
+ * @param options.user The user on whose behalf the operation is performed
1521
+ */
1402
1522
  touchAsync(id, name, options) {
1403
1523
  return new Promise((resolve, reject) => this.touch(id, name, options, err => (err ? reject(err) : resolve())));
1404
1524
  }
@@ -1414,7 +1534,7 @@ export class ObjectsInRedisClient {
1414
1534
  await this.client.del(id);
1415
1535
  }
1416
1536
  }
1417
- async _rm(id, name, options, meta) {
1537
+ async _rm(id, name, userContext, meta) {
1418
1538
  if (meta && !meta.isDir) {
1419
1539
  // it is a file
1420
1540
  const metaID = this.getFileId(id, name, true);
@@ -1447,30 +1567,30 @@ export class ObjectsInRedisClient {
1447
1567
  throw new Error(ERRORS.ERROR_NOT_FOUND);
1448
1568
  }
1449
1569
  // Check permissions
1450
- let objs;
1570
+ let strObjs;
1451
1571
  try {
1452
- objs = await this.client.mget(keys);
1572
+ strObjs = await this.client.mget(keys);
1453
1573
  }
1454
1574
  catch {
1455
1575
  // ignore
1456
1576
  }
1457
1577
  let result;
1458
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
1459
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
1460
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1461
- objs = objs || [];
1578
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
1579
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
1580
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1581
+ strObjs ||= [];
1462
1582
  if (!dontCheck) {
1463
1583
  result = [];
1464
1584
  for (let i = 0; i < keys.length; i++) {
1585
+ const strObj = strObjs[i];
1465
1586
  try {
1466
- const strObj = objs[i];
1467
1587
  const obj = strObj ? JSON.parse(strObj) : null;
1468
- if (utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
1588
+ if (utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
1469
1589
  result.push(keys[i]);
1470
1590
  }
1471
1591
  }
1472
1592
  catch {
1473
- this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${objs[i]}`);
1593
+ this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
1474
1594
  }
1475
1595
  }
1476
1596
  }
@@ -1494,26 +1614,31 @@ export class ObjectsInRedisClient {
1494
1614
  return files;
1495
1615
  }
1496
1616
  }
1617
+ /**
1618
+ * Delete a file or directory of an object
1619
+ *
1620
+ * @param id The id of the object owning the file
1621
+ * @param name The file or directory name to delete
1622
+ * @param options The current request options including the user, or the callback
1623
+ * @param callback Called with the list of removed files
1624
+ */
1497
1625
  rm(id, name, options, callback) {
1498
1626
  if (typeof options === 'function') {
1499
1627
  callback = options;
1500
1628
  options = null;
1501
1629
  }
1502
- if (options?.acl) {
1503
- options.acl = null;
1504
- }
1505
1630
  if (typeof name !== 'string') {
1506
1631
  name = '';
1507
1632
  }
1508
- this.checkFileRights(id, null, options, CONSTS.ACCESS_DELETE, async (err, options, meta) => {
1633
+ this.checkFileRights(id, null, options, CONSTS.ACCESS_DELETE, async (err, meta, userContext) => {
1509
1634
  if (err) {
1510
1635
  return tools.maybeCallbackWithError(callback, err);
1511
1636
  }
1512
- if (!options.acl.file.delete) {
1637
+ if (!userContext.acl.file.delete) {
1513
1638
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1514
1639
  }
1515
1640
  try {
1516
- const files = await this._rm(id, name, options, meta?.notExists ? null : meta);
1641
+ const files = await this._rm(id, name, userContext, meta?.notExists ? null : meta);
1517
1642
  return tools.maybeCallbackWithError(callback, null, files);
1518
1643
  }
1519
1644
  catch (e) {
@@ -1521,10 +1646,26 @@ export class ObjectsInRedisClient {
1521
1646
  }
1522
1647
  });
1523
1648
  }
1649
+ /**
1650
+ * Promise-version of rm
1651
+ *
1652
+ * @param id The id of the object owning the file
1653
+ * @param name The file or directory name to delete
1654
+ * @param options The current request options including the user
1655
+ * @param options.user The user on whose behalf the operation is performed
1656
+ */
1524
1657
  rmAsync(id, name, options) {
1525
1658
  return new Promise((resolve, reject) => this.rm(id, name, options, (err, files) => (err ? reject(err) : resolve(files))));
1526
1659
  }
1527
1660
  // simulate. redis has no dirs
1661
+ /**
1662
+ * Create a directory for an object's files (simulated, as redis has no real directories)
1663
+ *
1664
+ * @param id The id of the object owning the files
1665
+ * @param dirName The directory name to create
1666
+ * @param options The current request options including the user, or the callback
1667
+ * @param callback Called once the directory has been created
1668
+ */
1528
1669
  mkdir(id, dirName, options, callback) {
1529
1670
  if (typeof options === 'function') {
1530
1671
  callback = options;
@@ -1537,19 +1678,26 @@ export class ObjectsInRedisClient {
1537
1678
  if (dirName.startsWith('/')) {
1538
1679
  dirName = dirName.substring(1);
1539
1680
  }
1540
- this.checkFileRights(id, dirName, options, CONSTS.ACCESS_WRITE, (err, options) => {
1681
+ this.checkFileRights(id, dirName, options, CONSTS.ACCESS_WRITE, (err, meta, userContext) => {
1541
1682
  if (err) {
1542
1683
  return tools.maybeCallbackWithError(callback, err);
1543
1684
  }
1544
- if (!options.acl.file.write) {
1685
+ if (!userContext.acl.file.write) {
1545
1686
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1546
1687
  }
1547
- // we create a dummy file (for file this file exists to store meta data) - do not override passed options
1548
- options = { ...options, virtualFile: true };
1688
+ // we create a dummy file (for file this file exists to store metadata) - do not override passed options
1689
+ meta = { ...options, virtualFile: true };
1549
1690
  const realName = dirName + (dirName.endsWith('/') ? '' : '/');
1550
- this.writeFile(id, `${realName}_data.json`, '', options, callback);
1691
+ return this.writeFile(id, `${realName}_data.json`, '', meta, callback);
1551
1692
  });
1552
1693
  }
1694
+ /**
1695
+ * Promise-version of mkdir
1696
+ *
1697
+ * @param id The id of the object owning the files
1698
+ * @param dirName The directory name to create
1699
+ * @param options The current request options including the user
1700
+ */
1553
1701
  mkdirAsync(id, dirName, options) {
1554
1702
  return new Promise((resolve, reject) => this.mkdir(id, dirName, options, err => (err ? reject(err) : resolve())));
1555
1703
  }
@@ -1573,7 +1721,7 @@ export class ObjectsInRedisClient {
1573
1721
  }
1574
1722
  return tools.maybeCallback(callback);
1575
1723
  }
1576
- async _chownFile(id, name, options, callback, meta) {
1724
+ async _chownFile(id, name, options, meta, userContext, callback) {
1577
1725
  if (!meta) {
1578
1726
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND);
1579
1727
  }
@@ -1582,7 +1730,7 @@ export class ObjectsInRedisClient {
1582
1730
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
1583
1731
  }
1584
1732
  if (!meta.isDir && !meta.notExists) {
1585
- // it is file
1733
+ // it is a file
1586
1734
  const metaID = this.getFileId(id, name, true);
1587
1735
  meta.acl.owner = options.owner;
1588
1736
  meta.acl.ownerGroup = options.ownerGroup;
@@ -1597,7 +1745,7 @@ export class ObjectsInRedisClient {
1597
1745
  const res = [
1598
1746
  {
1599
1747
  path: nameArr.join('/'),
1600
- file: file,
1748
+ file,
1601
1749
  stats: meta.stats,
1602
1750
  isDir: false,
1603
1751
  acl: meta.acl || {},
@@ -1637,9 +1785,9 @@ export class ObjectsInRedisClient {
1637
1785
  catch (e) {
1638
1786
  return tools.maybeCallbackWithRedisError(callback, e);
1639
1787
  }
1640
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
1641
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
1642
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1788
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
1789
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
1790
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1643
1791
  const keysFiltered = [];
1644
1792
  const objsFiltered = [];
1645
1793
  const processed = [];
@@ -1655,7 +1803,7 @@ export class ObjectsInRedisClient {
1655
1803
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${metaStr}`);
1656
1804
  continue;
1657
1805
  }
1658
- if (dontCheck || utils.checkObject(meta, options, CONSTS.ACCESS_WRITE)) {
1806
+ if (dontCheck || utils.checkObject(meta, userContext, CONSTS.ACCESS_WRITE)) {
1659
1807
  if (!meta || meta.virtualFile) {
1660
1808
  continue;
1661
1809
  } // virtual file, ignore
@@ -1675,39 +1823,43 @@ export class ObjectsInRedisClient {
1675
1823
  });
1676
1824
  }
1677
1825
  }
1678
- this._chownFileHelper(keysFiltered, objsFiltered, options, err => {
1826
+ return this._chownFileHelper(keysFiltered, objsFiltered, options, err => {
1679
1827
  return tools.maybeCallbackWithError(callback, err, processed);
1680
1828
  });
1681
1829
  }
1830
+ /**
1831
+ * Change the owner and owner group of a file
1832
+ *
1833
+ * @param id The id of the object owning the file
1834
+ * @param name The file name
1835
+ * @param options The current request options including the new owner and the user
1836
+ * @param options.owner The new owner (user id) to assign to the file
1837
+ * @param options.user The user on whose behalf the operation is performed
1838
+ * @param options.ownerGroup The new owner group to assign to the file
1839
+ * @param options.group The new owner group to assign (alias for ownerGroup)
1840
+ * @param callback Called with the processed file(s)
1841
+ */
1682
1842
  chownFile(id, name, options, callback) {
1683
- if (typeof options === 'function') {
1684
- callback = options;
1685
- options = {};
1686
- }
1687
- options = options || {};
1688
- if (typeof options !== 'object') {
1689
- options = { owner: options };
1690
- }
1691
1843
  if (typeof name !== 'string' || !name.length || name === '/') {
1692
1844
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND);
1693
1845
  }
1694
1846
  if (name.startsWith('/')) {
1695
1847
  name = name.substring(1);
1696
1848
  }
1697
- if (!options.ownerGroup && options.group) {
1849
+ if (options?.group && !options.ownerGroup) {
1698
1850
  options.ownerGroup = options.group;
1699
1851
  }
1700
- if (!options.owner && options.user) {
1852
+ if (options?.user && !options.owner) {
1701
1853
  options.owner = options.user;
1702
1854
  }
1703
- if (!options.owner) {
1855
+ if (!options?.owner) {
1704
1856
  this.log.error(`${this.namespace} user is not defined`);
1705
1857
  return tools.maybeCallbackWithError(callback, 'invalid parameter');
1706
1858
  }
1707
1859
  if (!options.ownerGroup) {
1708
1860
  // get user group
1709
1861
  this.getUserGroup(options.owner, (user, groups) => {
1710
- if (!groups || !groups[0]) {
1862
+ if (!groups?.[0]) {
1711
1863
  return tools.maybeCallbackWithError(callback, `user "${options.owner}" belongs to no group`);
1712
1864
  }
1713
1865
  options.ownerGroup = groups[0];
@@ -1715,16 +1867,26 @@ export class ObjectsInRedisClient {
1715
1867
  });
1716
1868
  return;
1717
1869
  }
1718
- this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, options, meta) => {
1870
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, meta, userContext) => {
1719
1871
  if (err) {
1720
1872
  return tools.maybeCallbackWithError(callback, err);
1721
1873
  }
1722
- if (!options.acl.file.write) {
1874
+ if (!userContext.acl.file.write) {
1723
1875
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1724
1876
  }
1725
- return this._chownFile(id, name, options, callback, meta);
1877
+ return this._chownFile(id, name, options, meta, userContext, callback);
1726
1878
  });
1727
1879
  }
1880
+ /**
1881
+ * Promise-version of chownFile
1882
+ *
1883
+ * @param id The id of the object owning the file
1884
+ * @param name The file name
1885
+ * @param options The current request options including the new owner and the user
1886
+ * @param options.user The user on whose behalf the operation is performed
1887
+ * @param options.owner The new owner (user id) to assign to the file
1888
+ * @param options.ownerGroup The new owner group to assign to the file
1889
+ */
1728
1890
  chownFileAsync(id, name, options) {
1729
1891
  return new Promise((resolve, reject) => this.chownFile(id, name, options, (err, processed) => (err ? reject(err) : resolve(processed))));
1730
1892
  }
@@ -1733,6 +1895,8 @@ export class ObjectsInRedisClient {
1733
1895
  * @param keys Key names to handle
1734
1896
  * @param metas Objects for the keys to handle
1735
1897
  * @param options options
1898
+ * @param options.user The user on whose behalf the operation is performed
1899
+ * @param options.mode The new permission mode to apply
1736
1900
  * @param callback callback function
1737
1901
  */
1738
1902
  async _chmodFileHelper(keys, metas, options, callback) {
@@ -1742,7 +1906,7 @@ export class ObjectsInRedisClient {
1742
1906
  if (!this.client) {
1743
1907
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
1744
1908
  }
1745
- for (const i in keys) {
1909
+ for (let i = 0; i < keys.length; i++) {
1746
1910
  const id = keys[i];
1747
1911
  const meta = metas[i];
1748
1912
  meta.acl.permissions = options.mode;
@@ -1755,7 +1919,7 @@ export class ObjectsInRedisClient {
1755
1919
  }
1756
1920
  return tools.maybeCallback(callback);
1757
1921
  }
1758
- async _chmodFile(id, name, options, callback, meta) {
1922
+ async _chmodFile(id, name, options, meta, userContext, callback) {
1759
1923
  if (!meta) {
1760
1924
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND);
1761
1925
  }
@@ -1764,7 +1928,7 @@ export class ObjectsInRedisClient {
1764
1928
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
1765
1929
  }
1766
1930
  if (!meta.isDir && !meta.notExists) {
1767
- // it is file
1931
+ // it is a file
1768
1932
  const metaID = this.getFileId(id, name, true);
1769
1933
  meta.acl.permissions = options.mode;
1770
1934
  try {
@@ -1778,7 +1942,7 @@ export class ObjectsInRedisClient {
1778
1942
  const res = [
1779
1943
  {
1780
1944
  path: nameArr.join('/'),
1781
- file: file,
1945
+ file,
1782
1946
  stats: meta.stats,
1783
1947
  isDir: false,
1784
1948
  acl: meta.acl || {},
@@ -1818,9 +1982,9 @@ export class ObjectsInRedisClient {
1818
1982
  catch (e) {
1819
1983
  return tools.maybeCallbackWithRedisError(callback, e);
1820
1984
  }
1821
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
1822
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
1823
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1985
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
1986
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
1987
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
1824
1988
  const keysFiltered = [];
1825
1989
  const objsFiltered = [];
1826
1990
  const processed = [];
@@ -1836,7 +2000,7 @@ export class ObjectsInRedisClient {
1836
2000
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
1837
2001
  continue;
1838
2002
  }
1839
- if (dontCheck || utils.checkObject(obj, options, CONSTS.ACCESS_WRITE)) {
2003
+ if (dontCheck || utils.checkObject(obj, userContext, CONSTS.ACCESS_WRITE)) {
1840
2004
  if (!obj || obj.virtualFile) {
1841
2005
  continue;
1842
2006
  } // virtual file, ignore
@@ -1856,14 +2020,22 @@ export class ObjectsInRedisClient {
1856
2020
  });
1857
2021
  }
1858
2022
  }
1859
- this._chmodFileHelper(keysFiltered, objsFiltered, options, err => tools.maybeCallbackWithError(callback, err, processed));
2023
+ return this._chmodFileHelper(keysFiltered, objsFiltered, options, err => tools.maybeCallbackWithError(callback, err, processed));
1860
2024
  }
2025
+ /**
2026
+ * Change the file mode (permissions) of a single file
2027
+ *
2028
+ * @param id The id of the object owning the file
2029
+ * @param name The file name
2030
+ * @param options The current request options including the new mode and the user, or the callback
2031
+ * @param options.user The user on whose behalf the operation is performed
2032
+ * @param options.mode The new permission mode to apply
2033
+ * @param callback Called with the processed file
2034
+ */
1861
2035
  chmodFile(id, name, options, callback) {
1862
- if (typeof options === 'function') {
1863
- callback = options;
1864
- options = null;
2036
+ if (!options || options.mode === undefined) {
2037
+ throw new Error('options.mode is not defined');
1865
2038
  }
1866
- options = options || {};
1867
2039
  if (typeof name !== 'string' || !name.length || name === '/') {
1868
2040
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND);
1869
2041
  }
@@ -1880,28 +2052,41 @@ export class ObjectsInRedisClient {
1880
2052
  else if (typeof options.mode === 'string') {
1881
2053
  options.mode = parseInt(options.mode, 16);
1882
2054
  }
1883
- this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, options, meta) => {
2055
+ this.checkFileRights(id, name, options, CONSTS.ACCESS_WRITE, (err, meta, userContext) => {
1884
2056
  if (err) {
1885
2057
  return tools.maybeCallbackWithError(callback, err);
1886
2058
  }
1887
- if (!options.acl.file.write) {
2059
+ if (!userContext.acl.file.write) {
1888
2060
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
1889
2061
  }
1890
- return this._chmodFile(id, name, options, callback, meta);
2062
+ return this._chmodFile(id, name, options, meta, userContext, callback);
1891
2063
  });
1892
2064
  }
2065
+ /**
2066
+ * Promise-version of chmodFile
2067
+ *
2068
+ * @param id The id of the object owning the file
2069
+ * @param name The file name
2070
+ * @param options The current request options including the new mode and the user
2071
+ * @param options.user The user on whose behalf the operation is performed
2072
+ * @param options.mode The new permission mode to apply
2073
+ */
1893
2074
  chmodFileAsync(id, name, options) {
1894
2075
  return new Promise((resolve, reject) => this.chmodFile(id, name, options, (err, processed) => (err ? reject(err) : resolve(processed))));
1895
2076
  }
2077
+ /**
2078
+ * Enable or disable the file cache
2079
+ *
2080
+ * @param enabled Whether the file cache should be enabled
2081
+ * @param options The current request options including the user, or the callback
2082
+ * @param callback Called with the resulting cache state
2083
+ */
1896
2084
  enableFileCache(enabled, options, callback) {
1897
2085
  if (typeof options === 'function') {
1898
2086
  callback = options;
1899
2087
  options = null;
1900
2088
  }
1901
- if (options?.acl) {
1902
- options.acl = null;
1903
- }
1904
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, _options) => {
2089
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, err => {
1905
2090
  if (err) {
1906
2091
  return tools.maybeCallbackWithError(callback, err);
1907
2092
  }
@@ -1909,6 +2094,12 @@ export class ObjectsInRedisClient {
1909
2094
  return tools.maybeCallbackWithError(callback, null, false);
1910
2095
  });
1911
2096
  }
2097
+ /**
2098
+ * Promise-version of enableFileCache
2099
+ *
2100
+ * @param enabled Whether the file cache should be enabled
2101
+ * @param options The current request options including the user
2102
+ */
1912
2103
  enableFileCacheAsync(enabled, options) {
1913
2104
  return new Promise((resolve, reject) => this.enableFileCache(enabled, options, (err, res) => (err ? reject(err) : resolve(res))));
1914
2105
  }
@@ -1958,6 +2149,13 @@ export class ObjectsInRedisClient {
1958
2149
  }
1959
2150
  }
1960
2151
  }
2152
+ /**
2153
+ * Subscribe a user to file changes of an object
2154
+ *
2155
+ * @param id The id of the object owning the files
2156
+ * @param pattern One or more file name patterns to subscribe to
2157
+ * @param options The current request options including the user
2158
+ */
1961
2159
  subscribeUserFile(id, pattern, options) {
1962
2160
  return new Promise((resolve, reject) => {
1963
2161
  utils.checkObjectRights(this, null, null, options, 'list', err => {
@@ -1972,6 +2170,13 @@ export class ObjectsInRedisClient {
1972
2170
  });
1973
2171
  });
1974
2172
  }
2173
+ /**
2174
+ * Unsubscribe a user from file changes of an object
2175
+ *
2176
+ * @param id The id of the object owning the files
2177
+ * @param pattern One or more file name patterns to unsubscribe from
2178
+ * @param options The current request options including the user
2179
+ */
1975
2180
  unsubscribeUserFile(id, pattern, options) {
1976
2181
  return new Promise((resolve, reject) => {
1977
2182
  utils.checkObjectRights(this, null, null, options, 'list', err => {
@@ -2025,6 +2230,13 @@ export class ObjectsInRedisClient {
2025
2230
  return this._subscribe(pattern, false, callback);
2026
2231
  });
2027
2232
  }
2233
+ /**
2234
+ * Subscribe to object changes matching the given pattern
2235
+ *
2236
+ * @param pattern One or more patterns to subscribe to
2237
+ * @param options The current request options including the user, or the callback
2238
+ * @param callback Called once the subscription is registered
2239
+ */
2028
2240
  subscribe(pattern, options, callback) {
2029
2241
  if (typeof options === 'function') {
2030
2242
  callback = options;
@@ -2032,9 +2244,22 @@ export class ObjectsInRedisClient {
2032
2244
  }
2033
2245
  return this.subscribeConfig(pattern, options, callback);
2034
2246
  }
2247
+ /**
2248
+ * Promise-version of subscribe
2249
+ *
2250
+ * @param pattern One or more patterns to subscribe to
2251
+ * @param options The current request options including the user
2252
+ */
2035
2253
  subscribeAsync(pattern, options) {
2036
2254
  return new Promise((resolve, reject) => this.subscribe(pattern, options, err => (err ? reject(err) : resolve())));
2037
2255
  }
2256
+ /**
2257
+ * Subscribe a user to object changes matching the given pattern
2258
+ *
2259
+ * @param pattern One or more patterns to subscribe to
2260
+ * @param options The current request options including the user, or the callback
2261
+ * @param callback Called once the subscription is registered
2262
+ */
2038
2263
  subscribeUser(pattern, options, callback) {
2039
2264
  if (typeof options === 'function') {
2040
2265
  callback = options;
@@ -2047,6 +2272,12 @@ export class ObjectsInRedisClient {
2047
2272
  return this._subscribe(pattern, true, callback);
2048
2273
  });
2049
2274
  }
2275
+ /**
2276
+ * Promise-version of subscribeUser
2277
+ *
2278
+ * @param pattern One or more patterns to subscribe to
2279
+ * @param options The current request options including the user
2280
+ */
2050
2281
  subscribeUserAsync(pattern, options) {
2051
2282
  return new Promise((resolve, reject) => this.subscribeUser(pattern, options, err => (err ? reject(err) : resolve())));
2052
2283
  }
@@ -2088,6 +2319,13 @@ export class ObjectsInRedisClient {
2088
2319
  }
2089
2320
  });
2090
2321
  }
2322
+ /**
2323
+ * Unsubscribe from object changes matching the given pattern
2324
+ *
2325
+ * @param pattern One or more patterns to unsubscribe from
2326
+ * @param options The current request options including the user, or the callback
2327
+ * @param callback Called once the subscription is removed
2328
+ */
2091
2329
  unsubscribe(pattern, options, callback) {
2092
2330
  if (typeof options === 'function') {
2093
2331
  callback = options;
@@ -2095,9 +2333,22 @@ export class ObjectsInRedisClient {
2095
2333
  }
2096
2334
  return this.unsubscribeConfig(pattern, options, callback);
2097
2335
  }
2336
+ /**
2337
+ * Promise-version of unsubscribe
2338
+ *
2339
+ * @param pattern One or more patterns to unsubscribe from
2340
+ * @param options The current request options including the user
2341
+ */
2098
2342
  unsubscribeAsync(pattern, options) {
2099
2343
  return new Promise((resolve, reject) => this.unsubscribe(pattern, options, err => (err ? reject(err) : resolve())));
2100
2344
  }
2345
+ /**
2346
+ * Unsubscribe a user from object changes matching the given pattern
2347
+ *
2348
+ * @param pattern One or more patterns to unsubscribe from
2349
+ * @param options The current request options including the user, or the callback
2350
+ * @param callback Called once the subscription is removed
2351
+ */
2101
2352
  unsubscribeUser(pattern, options, callback) {
2102
2353
  if (typeof options === 'function') {
2103
2354
  callback = options;
@@ -2116,6 +2367,12 @@ export class ObjectsInRedisClient {
2116
2367
  }
2117
2368
  });
2118
2369
  }
2370
+ /**
2371
+ * Promise-version of unsubscribeUser
2372
+ *
2373
+ * @param pattern One or more patterns to unsubscribe from
2374
+ * @param options The current request options including the user
2375
+ */
2119
2376
  unsubscribeUserAsync(pattern, options) {
2120
2377
  return new Promise((resolve, reject) => this.unsubscribeUser(pattern, options, err => (err ? reject(err) : resolve())));
2121
2378
  }
@@ -2153,7 +2410,7 @@ export class ObjectsInRedisClient {
2153
2410
  await this.client.publish(id, message);
2154
2411
  }
2155
2412
  }
2156
- _chownObject(pattern, options, callback) {
2413
+ _chownObject(pattern, options, userContext, callback) {
2157
2414
  this.getKeys(pattern, options, async (err, keys) => {
2158
2415
  if (err) {
2159
2416
  return tools.maybeCallbackWithError(callback, err);
@@ -2183,7 +2440,7 @@ export class ObjectsInRedisClient {
2183
2440
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
2184
2441
  continue;
2185
2442
  }
2186
- if (!obj || !utils.checkObject(obj, options, CONSTS.ACCESS_WRITE)) {
2443
+ if (!obj || !utils.checkObject(obj, userContext, CONSTS.ACCESS_WRITE)) {
2187
2444
  continue;
2188
2445
  }
2189
2446
  if (!obj.acl) {
@@ -2214,20 +2471,22 @@ export class ObjectsInRedisClient {
2214
2471
  return tools.maybeCallbackWithError(callback, null, filteredObjs);
2215
2472
  }, true);
2216
2473
  }
2474
+ /**
2475
+ * Change the owner and owner group of all objects matching the given pattern
2476
+ *
2477
+ * @param pattern The pattern of object ids whose owner should be changed
2478
+ * @param options The current request options including the new owner and the user, or the callback
2479
+ * @param options.user The user on whose behalf the operation is performed
2480
+ * @param options.owner The new owner (user id) to assign to the objects
2481
+ * @param options.ownerGroup The new owner group to assign to the objects
2482
+ * @param options.group The new owner group to assign (alias for ownerGroup)
2483
+ * @param callback Called with the list of changed objects
2484
+ */
2217
2485
  chownObject(pattern, options, callback) {
2218
- if (typeof options === 'function') {
2219
- callback = options;
2220
- options = {};
2221
- }
2222
- options = options || {};
2223
- options.acl = null;
2224
- if (typeof options !== 'object') {
2225
- options = { owner: options };
2226
- }
2227
- if (!options.ownerGroup && options.group) {
2486
+ if (options?.group && !options.ownerGroup) {
2228
2487
  options.ownerGroup = options.group;
2229
2488
  }
2230
- if (!options.owner && options.user) {
2489
+ if (options?.user && !options.owner) {
2231
2490
  options.owner = options.user;
2232
2491
  }
2233
2492
  if (!options.owner) {
@@ -2237,28 +2496,38 @@ export class ObjectsInRedisClient {
2237
2496
  if (!options.ownerGroup) {
2238
2497
  // get user group
2239
2498
  this.getUserGroup(options.owner, (user, groups /* , permissions*/) => {
2240
- if (!groups || !groups[0]) {
2499
+ if (!groups?.[0]) {
2241
2500
  return tools.maybeCallbackWithError(callback, `user "${options.owner}" belongs to no group`);
2242
2501
  }
2243
2502
  options.ownerGroup = groups[0];
2244
- this.chownObject(pattern, options, callback);
2503
+ return this.chownObject(pattern, options, callback);
2245
2504
  });
2246
2505
  return;
2247
2506
  }
2248
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, options) => {
2507
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, userContext) => {
2249
2508
  if (err) {
2250
2509
  return tools.maybeCallbackWithRedisError(callback, err);
2251
2510
  }
2252
- if (!options.acl.object || !options.acl.object.write) {
2511
+ if (!userContext.acl.object || !userContext.acl.object.write) {
2253
2512
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
2254
2513
  }
2255
- return this._chownObject(pattern, options, callback);
2514
+ return this._chownObject(pattern, options, userContext, callback);
2256
2515
  });
2257
2516
  }
2517
+ /**
2518
+ * Promise-version of chownObject
2519
+ *
2520
+ * @param pattern The pattern of object ids whose owner should be changed
2521
+ * @param options The current request options including the new owner and the user
2522
+ * @param options.user The user on whose behalf the operation is performed
2523
+ * @param options.owner The new owner (user id) to assign to the objects
2524
+ * @param options.ownerGroup The new owner group to assign to the objects
2525
+ * @param options.group The new owner group to assign (alias for ownerGroup)
2526
+ */
2258
2527
  chownObjectAsync(pattern, options) {
2259
2528
  return new Promise((resolve, reject) => this.chownObject(pattern, options, (err, list) => (err ? reject(err) : resolve(list))));
2260
2529
  }
2261
- _chmodObject(pattern, options, callback) {
2530
+ _chmodObject(pattern, options, userContext, callback) {
2262
2531
  this.getKeys(pattern, options, async (err, keys) => {
2263
2532
  if (err) {
2264
2533
  return tools.maybeCallbackWithRedisError(callback, err);
@@ -2288,7 +2557,7 @@ export class ObjectsInRedisClient {
2288
2557
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${strObj}`);
2289
2558
  continue;
2290
2559
  }
2291
- if (!utils.checkObject(obj, options, CONSTS.ACCESS_WRITE) || !obj) {
2560
+ if (!utils.checkObject(obj, userContext, CONSTS.ACCESS_WRITE) || !obj) {
2292
2561
  continue;
2293
2562
  }
2294
2563
  if (!obj.acl) {
@@ -2321,15 +2590,21 @@ export class ObjectsInRedisClient {
2321
2590
  }
2322
2591
  }, true);
2323
2592
  }
2593
+ /**
2594
+ * Change the file mode (permissions) of all files matching the given pattern
2595
+ *
2596
+ * @param pattern The pattern of object ids whose files should be changed
2597
+ * @param options The current request options including the new mode and the user, or the callback
2598
+ * @param options.mode The new permission mode to apply
2599
+ * @param options.user The user on whose behalf the operation is performed
2600
+ * @param options.object The permission bitmask for object access
2601
+ * @param options.state The permission bitmask for state access
2602
+ * @param callback Called with the list of changed objects
2603
+ */
2324
2604
  chmodObject(pattern, options, callback) {
2325
- if (typeof options === 'function') {
2326
- callback = options;
2327
- options = null;
2328
- }
2329
- options = options || {};
2330
- options.acl = null;
2331
- if (typeof options !== 'object') {
2332
- options = { object: options };
2605
+ if (!options) {
2606
+ this.log.error(`${this.namespace} mode is not defined`);
2607
+ return tools.maybeCallbackWithError(callback, 'invalid parameter');
2333
2608
  }
2334
2609
  if (options.mode && !options.object) {
2335
2610
  options.object = options.mode;
@@ -2341,20 +2616,30 @@ export class ObjectsInRedisClient {
2341
2616
  else if (typeof options.mode === 'string') {
2342
2617
  options.mode = parseInt(options.mode, 16);
2343
2618
  }
2344
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, options) => {
2619
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, userContext) => {
2345
2620
  if (err) {
2346
2621
  return tools.maybeCallbackWithRedisError(callback, err);
2347
2622
  }
2348
- if (!options.acl.file.write) {
2623
+ if (!userContext.acl.file.write) {
2349
2624
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
2350
2625
  }
2351
- return this._chmodObject(pattern, options, callback);
2626
+ return this._chmodObject(pattern, options, userContext, callback);
2352
2627
  });
2353
2628
  }
2629
+ /**
2630
+ * Promise-version of chmodObject
2631
+ *
2632
+ * @param pattern The pattern of object ids whose files should be changed
2633
+ * @param options The current request options including the new mode and the user
2634
+ * @param options.mode The new permission mode to apply
2635
+ * @param options.user The user on whose behalf the operation is performed
2636
+ * @param options.object The permission bitmask for object access
2637
+ * @param options.state The permission bitmask for state access
2638
+ */
2354
2639
  chmodObjectAsync(pattern, options) {
2355
2640
  return new Promise((resolve, reject) => this.chmodObject(pattern, options, (err, list) => (err ? reject(err) : resolve(list))));
2356
2641
  }
2357
- async _getObject(id, options, callback) {
2642
+ async _getObject(id, userContext, callback) {
2358
2643
  if (!this.client) {
2359
2644
  return tools.maybeCallbackWithRedisError(callback, ERRORS.ERROR_DB_CLOSED);
2360
2645
  }
@@ -2381,13 +2666,20 @@ export class ObjectsInRedisClient {
2381
2666
  }
2382
2667
  if (obj) {
2383
2668
  // Check permissions
2384
- if (utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
2669
+ if (utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
2385
2670
  return tools.maybeCallbackWithError(callback, null, obj);
2386
2671
  }
2387
2672
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
2388
2673
  }
2389
2674
  return tools.maybeCallbackWithRedisError(callback, err, obj);
2390
2675
  }
2676
+ /**
2677
+ * Get a single object by its id
2678
+ *
2679
+ * @param id The id of the object to read
2680
+ * @param options The current request options including the user, or the callback
2681
+ * @param callback Called with the read object
2682
+ */
2391
2683
  getObject(id, options, callback) {
2392
2684
  if (typeof options === 'function') {
2393
2685
  callback = options;
@@ -2397,27 +2689,25 @@ export class ObjectsInRedisClient {
2397
2689
  return new Promise((resolve, reject) => this.getObject(id, options, (err, obj) => (err ? reject(err) : resolve(obj))));
2398
2690
  }
2399
2691
  if (typeof callback === 'function') {
2400
- if (options?.acl) {
2401
- options.acl = null;
2402
- }
2403
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, options) => {
2692
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, userContext) => {
2404
2693
  if (err) {
2405
2694
  return tools.maybeCallbackWithError(callback, err);
2406
2695
  }
2407
- return this._getObject(id, options, callback);
2696
+ return this._getObject(id, userContext, callback);
2408
2697
  });
2409
2698
  }
2410
2699
  }
2411
2700
  /**
2701
+ * Promise-version of getObject
2412
2702
  *
2413
- * @param id
2414
- * @param options
2703
+ * @param id The id of the object to read
2704
+ * @param options The current request options including the user
2415
2705
  * @deprecated use `getObject` without callback instead
2416
2706
  */
2417
2707
  getObjectAsync(id, options) {
2418
2708
  return new Promise((resolve, reject) => this.getObject(id, options, (err, obj) => (err ? reject(err) : resolve(obj))));
2419
2709
  }
2420
- async _getKeys(pattern, options, callback, dontModify) {
2710
+ async _getKeys(pattern, userContext, dontModify, callback) {
2421
2711
  if (!this.client) {
2422
2712
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
2423
2713
  }
@@ -2439,9 +2729,9 @@ export class ObjectsInRedisClient {
2439
2729
  if (keys) {
2440
2730
  keys.sort();
2441
2731
  const result = [];
2442
- const dontCheck = options.user === CONSTS.SYSTEM_ADMIN_USER ||
2443
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
2444
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
2732
+ const dontCheck = userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
2733
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
2734
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP);
2445
2735
  if (dontCheck) {
2446
2736
  for (let i = 0; i < keys.length; i++) {
2447
2737
  const id = keys[i].substring(this.objNamespaceL);
@@ -2475,7 +2765,7 @@ export class ObjectsInRedisClient {
2475
2765
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${metaStr}`);
2476
2766
  continue;
2477
2767
  }
2478
- if (r.test(keys[i]) && utils.checkObject(meta, options, CONSTS.ACCESS_READ)) {
2768
+ if (r.test(keys[i]) && utils.checkObject(meta, userContext, CONSTS.ACCESS_READ)) {
2479
2769
  if (!dontModify) {
2480
2770
  result.push(keys[i].substring(this.objNamespaceL));
2481
2771
  }
@@ -2488,6 +2778,14 @@ export class ObjectsInRedisClient {
2488
2778
  }
2489
2779
  return tools.maybeCallbackWithError(callback, null, result);
2490
2780
  }
2781
+ /**
2782
+ * Get all object ids matching the given pattern
2783
+ *
2784
+ * @param pattern The pattern to match object ids against
2785
+ * @param options The current request options including the user, or the callback
2786
+ * @param callback Called with the matching keys
2787
+ * @param dontModify If true, the returned keys are not stripped of the namespace
2788
+ */
2491
2789
  getKeys(pattern, options, callback, dontModify) {
2492
2790
  if (typeof options === 'function') {
2493
2791
  callback = options;
@@ -2497,18 +2795,25 @@ export class ObjectsInRedisClient {
2497
2795
  return new Promise((resolve, reject) => this.getKeys(pattern, options, (err, obj) => (err ? reject(err) : resolve(obj)), dontModify));
2498
2796
  }
2499
2797
  if (typeof callback === 'function') {
2500
- utils.checkObjectRights(this, null, null, options, 'list', (err, options) => {
2798
+ utils.checkObjectRights(this, null, null, options, 'list', (err, userContext) => {
2501
2799
  if (err) {
2502
2800
  return tools.maybeCallbackWithRedisError(callback, err);
2503
2801
  }
2504
- return this._getKeys(pattern, options, callback, dontModify);
2802
+ return this._getKeys(pattern, userContext, dontModify, callback);
2505
2803
  });
2506
2804
  }
2507
2805
  }
2508
- getKeysAsync(id, options) {
2509
- return this.getKeys(id, options);
2806
+ /**
2807
+ * Promise-version of getKeys
2808
+ *
2809
+ * @param pattern The pattern to match object ids against
2810
+ * @param options The current request options including the user
2811
+ * @param options.user The user on whose behalf the keys are read
2812
+ */
2813
+ getKeysAsync(pattern, options) {
2814
+ return this.getKeys(pattern, options);
2510
2815
  }
2511
- async _getObjects(keys, options, callback, dontModify) {
2816
+ async _getObjects(keys, userContext, callback, dontModify) {
2512
2817
  if (!keys) {
2513
2818
  return tools.maybeCallbackWithError(callback, 'no keys');
2514
2819
  }
@@ -2540,10 +2845,10 @@ export class ObjectsInRedisClient {
2540
2845
  }
2541
2846
  let result = [];
2542
2847
  if (objs) {
2543
- const dontCheck = options &&
2544
- (options.user === CONSTS.SYSTEM_ADMIN_USER ||
2545
- options.group !== CONSTS.SYSTEM_ADMIN_GROUP ||
2546
- options.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP));
2848
+ const dontCheck = userContext &&
2849
+ (userContext.user === CONSTS.SYSTEM_ADMIN_USER ||
2850
+ userContext.group === CONSTS.SYSTEM_ADMIN_GROUP ||
2851
+ userContext.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP));
2547
2852
  if (!dontCheck) {
2548
2853
  for (let i = 0; i < objs.length; i++) {
2549
2854
  const strObj = objs[i];
@@ -2557,7 +2862,7 @@ export class ObjectsInRedisClient {
2557
2862
  result.push({ error: ERRORS.ERROR_PERMISSION });
2558
2863
  continue;
2559
2864
  }
2560
- if (utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
2865
+ if (utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
2561
2866
  result.push(obj);
2562
2867
  }
2563
2868
  else {
@@ -2579,6 +2884,14 @@ export class ObjectsInRedisClient {
2579
2884
  }
2580
2885
  return tools.maybeCallbackWithError(callback, null, result);
2581
2886
  }
2887
+ /**
2888
+ * Get multiple objects by their ids
2889
+ *
2890
+ * @param keys The ids of the objects to read
2891
+ * @param options The current request options including the user, or the callback
2892
+ * @param callback Called with the read objects
2893
+ * @param dontModify If true, the returned objects are not cloned/modified
2894
+ */
2582
2895
  getObjects(keys, options, callback, dontModify) {
2583
2896
  if (typeof options === 'function') {
2584
2897
  callback = options;
@@ -2589,22 +2902,25 @@ export class ObjectsInRedisClient {
2589
2902
  // @ts-expect-error need to clarify, that objs is not undefined if no error is provided
2590
2903
  this.getObjects(keys, options, (err, objs) => (err ? reject(err) : resolve(objs)), dontModify));
2591
2904
  }
2592
- if (options?.acl) {
2593
- options.acl = null;
2594
- }
2595
2905
  if (typeof callback === 'function') {
2596
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, options) => {
2906
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, userContext) => {
2597
2907
  if (err) {
2598
2908
  return tools.maybeCallbackWithRedisError(callback, err);
2599
2909
  }
2600
- return this._getObjects(keys, options, callback, dontModify);
2910
+ return this._getObjects(keys, userContext, callback, dontModify);
2601
2911
  });
2602
2912
  }
2603
2913
  }
2914
+ /**
2915
+ * Promise-version of getObjects
2916
+ *
2917
+ * @param keys The ids of the objects to read
2918
+ * @param options The current request options including the user
2919
+ */
2604
2920
  getObjectsAsync(keys, options) {
2605
2921
  return this.getObjects(keys, options);
2606
2922
  }
2607
- async _getObjectsByPattern(pattern, options, callback) {
2923
+ async _getObjectsByPattern(pattern, userContext, callback) {
2608
2924
  if (!pattern || typeof pattern !== 'string') {
2609
2925
  return tools.maybeCallbackWithError(callback, `invalid pattern ${JSON.stringify(pattern)}`);
2610
2926
  }
@@ -2624,8 +2940,15 @@ export class ObjectsInRedisClient {
2624
2940
  if (this.settings.connection.enhancedLogging) {
2625
2941
  this.log.silly(`${this.namespace} redis keys ${keys.length} ${pattern}`);
2626
2942
  }
2627
- this._getObjects(keys, options, callback, true);
2943
+ this._getObjects(keys, userContext, callback, true).catch(e => this.log.error(`${this.namespace} Cannot get objects: ${e.message}`));
2628
2944
  }
2945
+ /**
2946
+ * Get all objects whose id matches the given pattern
2947
+ *
2948
+ * @param pattern The pattern to match object ids against
2949
+ * @param options The current request options including the user, or the callback
2950
+ * @param callback Called with the matching objects
2951
+ */
2629
2952
  getObjectsByPattern(pattern, options, callback) {
2630
2953
  if (typeof options === 'function') {
2631
2954
  callback = options;
@@ -2634,22 +2957,26 @@ export class ObjectsInRedisClient {
2634
2957
  if (!callback) {
2635
2958
  return new Promise((resolve, reject) => this.getObjectsByPattern(pattern, options, (err, obj) => (err ? reject(err) : resolve(obj))));
2636
2959
  }
2637
- if (options?.acl) {
2638
- options.acl = null;
2639
- }
2640
2960
  if (typeof callback === 'function') {
2641
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, options) => {
2961
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_READ, (err, userContext) => {
2642
2962
  if (err) {
2643
2963
  return tools.maybeCallbackWithRedisError(callback, err);
2644
2964
  }
2645
- return this._getObjectsByPattern(pattern, options, callback);
2965
+ return this._getObjectsByPattern(pattern, userContext, callback);
2646
2966
  });
2647
2967
  }
2648
2968
  }
2969
+ /**
2970
+ * Promise-version of getObjectsByPattern
2971
+ *
2972
+ * @param pattern The pattern to match object ids against
2973
+ * @param options The current request options including the user
2974
+ * @param options.user The user on whose behalf the objects are read
2975
+ */
2649
2976
  getObjectsByPatternAsync(pattern, options) {
2650
2977
  return new Promise((resolve, reject) => this.getObjectsByPattern(pattern, options, (err, objs) => (err ? reject(err) : resolve(objs))));
2651
2978
  }
2652
- async _setObject(id, obj, options) {
2979
+ async _setObject(id, obj, userContext) {
2653
2980
  if (!id || typeof id !== 'string' || utils.REG_CHECK_ID.test(id)) {
2654
2981
  throw new Error(`Invalid ID: ${id}`);
2655
2982
  }
@@ -2658,7 +2985,7 @@ export class ObjectsInRedisClient {
2658
2985
  throw new Error('obj is null');
2659
2986
  }
2660
2987
  if (!tools.isObject(obj)) {
2661
- this.log.warn(`${this.namespace} setObject: Argument object is no object: ${obj}`);
2988
+ this.log.warn(`${this.namespace} setObject: Argument object is no object: ${JSON.stringify(obj)}`);
2662
2989
  throw new Error('obj is no object');
2663
2990
  }
2664
2991
  if (!this.client) {
@@ -2776,16 +3103,16 @@ export class ObjectsInRedisClient {
2776
3103
  delete obj.acl.state;
2777
3104
  }
2778
3105
  // take the current user as owner if given, but if admin we keep default
2779
- if (options.user && options.user !== CONSTS.SYSTEM_ADMIN_USER) {
2780
- obj.acl.owner = options.user;
3106
+ if (userContext.user && userContext.user !== CONSTS.SYSTEM_ADMIN_USER) {
3107
+ obj.acl.owner = userContext.user;
2781
3108
  }
2782
3109
  // take the current group as owner if given, but if admin we keep default
2783
- if (options.group && options.group !== CONSTS.SYSTEM_ADMIN_GROUP) {
2784
- obj.acl.ownerGroup = options.group;
3110
+ if (userContext.group && userContext.group !== CONSTS.SYSTEM_ADMIN_GROUP) {
3111
+ obj.acl.ownerGroup = userContext.group;
2785
3112
  }
2786
3113
  }
2787
- if (this.defaultNewAcl && obj.acl && !obj.acl.ownerGroup && options.group) {
2788
- obj.acl.ownerGroup = options.group;
3114
+ if (this.defaultNewAcl && obj.acl && !obj.acl.ownerGroup && userContext.group) {
3115
+ obj.acl.ownerGroup = userContext.group;
2789
3116
  }
2790
3117
  const message = JSON.stringify(obj);
2791
3118
  const commands = [];
@@ -2828,12 +3155,12 @@ export class ObjectsInRedisClient {
2828
3155
  return { id };
2829
3156
  }
2830
3157
  /**
2831
- * set anew or update object
3158
+ * set anew or update an object
2832
3159
  *
2833
3160
  * This function writes the object into DB
2834
3161
  *
2835
3162
  * @param id ID of the object
2836
- * @param obj
3163
+ * @param obj The object to write
2837
3164
  * @param options options for access control are optional
2838
3165
  * @param callback return function
2839
3166
  */
@@ -2845,16 +3172,13 @@ export class ObjectsInRedisClient {
2845
3172
  if (!callback) {
2846
3173
  return new Promise((resolve, reject) => this.setObject(id, obj, options, (err, res) => (err ? reject(err) : resolve(res))));
2847
3174
  }
2848
- if (options?.acl) {
2849
- options.acl = null;
2850
- }
2851
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, async (err) => {
3175
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, async (err, userContext) => {
2852
3176
  // do not use options from checkObjectRights because this will mess up configured default acl
2853
3177
  if (err) {
2854
3178
  return tools.maybeCallbackWithRedisError(callback, err);
2855
3179
  }
2856
3180
  try {
2857
- const res = await this._setObject(id, obj, options || {});
3181
+ const res = await this._setObject(id, obj, userContext);
2858
3182
  return tools.maybeCallbackWithError(callback, null, res);
2859
3183
  }
2860
3184
  catch (e) {
@@ -2863,10 +3187,11 @@ export class ObjectsInRedisClient {
2863
3187
  });
2864
3188
  }
2865
3189
  /**
3190
+ * Promise-version of setObject
2866
3191
  *
2867
- * @param id
2868
- * @param obj
2869
- * @param options
3192
+ * @param id ID of the object
3193
+ * @param obj The object to write
3194
+ * @param options options for access control are optional
2870
3195
  * @deprecated use `setObject` without callback instead
2871
3196
  */
2872
3197
  setObjectAsync(id, obj, options) {
@@ -2874,7 +3199,7 @@ export class ObjectsInRedisClient {
2874
3199
  // @ts-expect-error TODO we are returning type Object for ease of use to devs, but formally these are AnyObjects, e.g. not guaranteed to have common
2875
3200
  this.setObject(id, obj, options, (err, res) => (err ? reject(err) : resolve(res))));
2876
3201
  }
2877
- async _delObject(id, options) {
3202
+ async _delObject(id, userContext) {
2878
3203
  if (!id || typeof id !== 'string' || utils.REG_CHECK_ID.test(id)) {
2879
3204
  throw new Error(`Invalid ID: ${id}`);
2880
3205
  }
@@ -2903,7 +3228,7 @@ export class ObjectsInRedisClient {
2903
3228
  this.log.warn(`${this.namespace} Cannot parse ${id} - ${oldObjStr}: ${e.message}`);
2904
3229
  oldObj = null;
2905
3230
  }
2906
- if (!utils.checkObject(oldObj, options, CONSTS.ACCESS_WRITE)) {
3231
+ if (!utils.checkObject(oldObj, userContext, CONSTS.ACCESS_WRITE)) {
2907
3232
  throw new Error(ERRORS.ERROR_PERMISSION);
2908
3233
  }
2909
3234
  else {
@@ -2935,23 +3260,27 @@ export class ObjectsInRedisClient {
2935
3260
  await this.client.publish(this.objNamespace + id, 'null');
2936
3261
  }
2937
3262
  }
3263
+ /**
3264
+ * Delete an object
3265
+ *
3266
+ * @param id The id of the object to delete
3267
+ * @param options The current request options including the user, or the callback
3268
+ * @param callback Called once the object has been deleted
3269
+ */
2938
3270
  delObject(id, options, callback) {
2939
3271
  if (typeof options === 'function') {
2940
3272
  callback = options;
2941
3273
  options = null;
2942
3274
  }
2943
3275
  if (!callback) {
2944
- return new Promise((resolve, reject) => this.delObject(id, options, err => (err ? reject(err) : resolve())));
3276
+ return new Promise((resolve, reject) => this.delObject(id, options, err => err ? reject(err) : resolve()));
2945
3277
  }
2946
- if (options?.acl) {
2947
- options.acl = null;
2948
- }
2949
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_DELETE, async (err, options) => {
3278
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_DELETE, async (err, userContext) => {
2950
3279
  if (err) {
2951
3280
  return tools.maybeCallbackWithError(callback, err);
2952
3281
  }
2953
3282
  try {
2954
- await this._delObject(id, options);
3283
+ await this._delObject(id, userContext);
2955
3284
  return tools.maybeCallback(callback);
2956
3285
  }
2957
3286
  catch (e) {
@@ -2959,11 +3288,18 @@ export class ObjectsInRedisClient {
2959
3288
  }
2960
3289
  });
2961
3290
  }
3291
+ /**
3292
+ * Promise-version of delObject
3293
+ *
3294
+ * @param id The id of the object to delete
3295
+ * @param options The current request options including the user
3296
+ * @param options.user The user on whose behalf the operation is performed
3297
+ */
2962
3298
  delObjectAsync(id, options) {
2963
3299
  return this.delObject(id, options);
2964
3300
  }
2965
3301
  /**
2966
- * Function to checks if comparisons will work according to the configured Locale
3302
+ * Function to check if comparisons will work according to the configured Locale
2967
3303
  */
2968
3304
  async isSystemLocaleSupported() {
2969
3305
  if (!this.client) {
@@ -2979,15 +3315,13 @@ export class ObjectsInRedisClient {
2979
3315
  }
2980
3316
  }
2981
3317
  // this function is very ineffective. Because reads all objects and then process them
2982
- async _applyViewFunc(func, params, options = {}) {
3318
+ async _applyViewFunc(func, params, userContext) {
2983
3319
  if (!this.client) {
2984
3320
  throw new Error(ERRORS.ERROR_DB_CLOSED);
2985
3321
  }
2986
- const result = {
2987
- rows: [],
2988
- };
3322
+ let rows = [];
2989
3323
  /**
2990
- * filters objs which are already present (and parse Errors) in array by property 'id'
3324
+ * filters objs which are already present (and parse Errors) in an array by property 'id'
2991
3325
  *
2992
3326
  * @param arr - Array of objects which should be filtered
2993
3327
  * @param duplicateFiltering - if duplicates need to be filtered
@@ -3008,9 +3342,9 @@ export class ObjectsInRedisClient {
3008
3342
  return obj.value !== null;
3009
3343
  });
3010
3344
  };
3011
- params = params || {};
3012
- params.startkey = params.startkey || '';
3013
- params.endkey = params.endkey || '\u9999';
3345
+ params ||= {};
3346
+ params.startkey ||= '';
3347
+ params.endkey ||= '\u9999';
3014
3348
  const wildcardPos = params.endkey.indexOf('\u9999');
3015
3349
  let wildCardLastPos = true;
3016
3350
  if (wildcardPos !== -1 && wildcardPos !== params.endkey.length - 1) {
@@ -3031,16 +3365,7 @@ export class ObjectsInRedisClient {
3031
3365
  }
3032
3366
  let objs;
3033
3367
  try {
3034
- objs = await this.client.evalsha([
3035
- this.scripts.filter,
3036
- 6,
3037
- this.objNamespace,
3038
- params.startkey,
3039
- params.endkey,
3040
- matches[1],
3041
- cursor,
3042
- `${this.setNamespace}object.type.${matches[1]}`,
3043
- ]);
3368
+ objs = (await this.client.evalsha(this.scripts.filter, 6, this.objNamespace, params.startkey, params.endkey, matches[1], cursor, `${this.setNamespace}object.type.${matches[1]}`));
3044
3369
  }
3045
3370
  catch (e) {
3046
3371
  this.log.warn(`${this.namespace} Cannot get view: ${e.message}`);
@@ -3072,7 +3397,7 @@ export class ObjectsInRedisClient {
3072
3397
  if (matches[2] && matches[2].trim() === 'doc._id') {
3073
3398
  return { id: obj._id, value: obj };
3074
3399
  }
3075
- else if (matches[2] && matches[2].trim() === 'doc.common.name' && obj.common) {
3400
+ if (matches[2] && matches[2].trim() === 'doc.common.name' && obj.common) {
3076
3401
  if (typeof obj.common.name === 'object') {
3077
3402
  if (obj.common.name.en) {
3078
3403
  return { id: obj.common.name.en, value: obj };
@@ -3085,29 +3410,30 @@ export class ObjectsInRedisClient {
3085
3410
  return { id: 'parseError', value: null };
3086
3411
  });
3087
3412
  if (currRows.length) {
3088
- result.rows = [...result.rows, ...currRows];
3413
+ rows = [...rows, ...currRows];
3089
3414
  } // endIf
3090
3415
  } while (cursor !== '0');
3091
3416
  // Now we have all objects -> calculate max if desired
3092
3417
  if (func.reduce === '_stats') {
3093
3418
  let max = null;
3094
- for (let i = 0; i < result.rows.length; i++) {
3095
- if (max === null || result.rows[i].value > max) {
3096
- max = result.rows[i].value;
3419
+ for (let i = 0; i < rows.length; i++) {
3420
+ const _value = rows[i].value;
3421
+ if (max === null || (_value !== null && _value > max)) {
3422
+ max = _value;
3097
3423
  }
3098
3424
  }
3099
3425
  if (max !== null) {
3100
- result.rows = [{ id: '_stats', value: { max: max } }];
3426
+ // @ts-expect-error missing typing
3427
+ rows = [{ id: '_stats', value: { max } }];
3101
3428
  }
3102
3429
  else {
3103
- result.rows = [];
3430
+ rows = [];
3104
3431
  }
3105
3432
  }
3106
3433
  // apply filter if needed
3107
- result.rows = filterEntries(result.rows, filterRequired);
3108
- return result;
3434
+ return { rows: filterEntries(rows, filterRequired) };
3109
3435
  }
3110
- else if (
3436
+ if (
3111
3437
  // filter by script
3112
3438
  wildCardLastPos &&
3113
3439
  func?.map &&
@@ -3121,15 +3447,7 @@ export class ObjectsInRedisClient {
3121
3447
  }
3122
3448
  let res;
3123
3449
  try {
3124
- res = await this.client.evalsha([
3125
- this.scripts.script,
3126
- 5,
3127
- this.objNamespace,
3128
- params.startkey,
3129
- params.endkey,
3130
- cursor,
3131
- `${this.setNamespace}object.type.script`,
3132
- ]);
3450
+ res = (await this.client.evalsha(this.scripts.script, 5, this.objNamespace, params.startkey, params.endkey, cursor, `${this.setNamespace}object.type.script`));
3133
3451
  }
3134
3452
  catch (e) {
3135
3453
  this.log.warn(`${this.namespace} Cannot get "scripts" view: ${e.message}`);
@@ -3157,14 +3475,13 @@ export class ObjectsInRedisClient {
3157
3475
  }
3158
3476
  });
3159
3477
  if (currRows.length) {
3160
- result.rows = [...result.rows, ...currRows];
3478
+ rows = [...rows, ...currRows];
3161
3479
  } // endIf
3162
3480
  } while (cursor !== '0');
3163
3481
  // apply filter if needed
3164
- result.rows = filterEntries(result.rows, filterRequired);
3165
- return result;
3482
+ return { rows: filterEntries(rows, filterRequired) };
3166
3483
  }
3167
- else if (
3484
+ if (
3168
3485
  // filter by hm-rega programs
3169
3486
  wildCardLastPos &&
3170
3487
  func?.map &&
@@ -3178,15 +3495,7 @@ export class ObjectsInRedisClient {
3178
3495
  }
3179
3496
  let objs;
3180
3497
  try {
3181
- objs = await this.client.evalsha([
3182
- this.scripts.programs,
3183
- 5,
3184
- `${this.objNamespace}hm-rega.`,
3185
- params.startkey,
3186
- params.endkey,
3187
- cursor,
3188
- `${this.setNamespace}object.type.channel`,
3189
- ]);
3498
+ objs = (await this.client.evalsha(this.scripts.programs, 5, `${this.objNamespace}hm-rega.`, params.startkey, params.endkey, cursor, `${this.setNamespace}object.type.channel`));
3190
3499
  }
3191
3500
  catch (e) {
3192
3501
  this.log.warn(`${this.namespace} Cannot get view: ${e.message}`);
@@ -3212,14 +3521,13 @@ export class ObjectsInRedisClient {
3212
3521
  }
3213
3522
  });
3214
3523
  if (currRows.length) {
3215
- result.rows = [...result.rows, ...currRows];
3524
+ rows = [...rows, ...currRows];
3216
3525
  } // endIf
3217
3526
  } while (cursor !== '0');
3218
3527
  // apply filter if needed
3219
- result.rows = filterEntries(result.rows, filterRequired);
3220
- return result;
3528
+ return { rows: filterEntries(rows, filterRequired) };
3221
3529
  }
3222
- else if (
3530
+ if (
3223
3531
  // filter by hm-rega variables
3224
3532
  wildCardLastPos &&
3225
3533
  func?.map &&
@@ -3233,15 +3541,7 @@ export class ObjectsInRedisClient {
3233
3541
  }
3234
3542
  let objs;
3235
3543
  try {
3236
- objs = await this.client.evalsha([
3237
- this.scripts.variables,
3238
- 5,
3239
- `${this.objNamespace}hm-rega.`,
3240
- params.startkey,
3241
- params.endkey,
3242
- cursor,
3243
- `${this.setNamespace}object.type.state`,
3244
- ]);
3544
+ objs = (await this.client.evalsha(this.scripts.variables, 5, `${this.objNamespace}hm-rega.`, params.startkey, params.endkey, cursor, `${this.setNamespace}object.type.state`));
3245
3545
  }
3246
3546
  catch (e) {
3247
3547
  this.log.warn(`${this.namespace} Cannot get view ${e.message}`);
@@ -3267,14 +3567,13 @@ export class ObjectsInRedisClient {
3267
3567
  }
3268
3568
  });
3269
3569
  if (currRows.length) {
3270
- result.rows = [...result.rows, ...currRows];
3570
+ rows = [...rows, ...currRows];
3271
3571
  } // endIf
3272
3572
  } while (cursor !== '0');
3273
3573
  // apply filter if needed
3274
- result.rows = filterEntries(result.rows, filterRequired);
3275
- return result;
3574
+ return { rows: filterEntries(rows, filterRequired) };
3276
3575
  }
3277
- else if (
3576
+ if (
3278
3577
  // filter by custom, redis also returns if common.custom is not present
3279
3578
  wildCardLastPos &&
3280
3579
  func?.map &&
@@ -3288,15 +3587,7 @@ export class ObjectsInRedisClient {
3288
3587
  }
3289
3588
  let objs;
3290
3589
  try {
3291
- objs = await this.client.evalsha([
3292
- this.scripts.custom,
3293
- 5,
3294
- this.objNamespace,
3295
- params.startkey,
3296
- params.endkey,
3297
- cursor,
3298
- `${this.setNamespace}object.common.custom`,
3299
- ]);
3590
+ objs = (await this.client.evalsha(this.scripts.custom, 5, this.objNamespace, params.startkey, params.endkey, cursor, `${this.setNamespace}object.common.custom`));
3300
3591
  }
3301
3592
  catch (e) {
3302
3593
  this.log.warn(`${this.namespace} Cannot get view: ${e.message}`);
@@ -3323,17 +3614,17 @@ export class ObjectsInRedisClient {
3323
3614
  }
3324
3615
  if (obj?.common?.custom) {
3325
3616
  if (useFullObject) {
3326
- result.rows.push({ id: obj._id, value: obj });
3617
+ rows.push({ id: obj._id, value: obj });
3327
3618
  }
3328
3619
  else {
3329
- result.rows.push({ id: obj._id, value: obj.common.custom });
3620
+ // @ts-expect-error missing typing
3621
+ rows.push({ id: obj._id, value: obj.common.custom });
3330
3622
  }
3331
3623
  }
3332
3624
  }
3333
3625
  } while (cursor !== '0');
3334
3626
  // apply filter if needed
3335
- result.rows = filterEntries(result.rows, filterRequired);
3336
- return result;
3627
+ return { rows: filterEntries(rows, filterRequired) };
3337
3628
  }
3338
3629
  if (!wildCardLastPos) {
3339
3630
  this.log.debug(`${this.namespace} Search can't be optimized because wildcard not at the end, fallback to keys!: ${func.map}`);
@@ -3381,7 +3672,7 @@ export class ObjectsInRedisClient {
3381
3672
  objs = [];
3382
3673
  }
3383
3674
  const _emit_ = (id, obj) => {
3384
- result.rows.push({ id: id, value: obj });
3675
+ rows.push({ id: id, value: obj });
3385
3676
  };
3386
3677
  const f = eval(`(${func.map.replace(/^function\(([a-z0-9A-Z_]+)\)/g, 'function($1, emit)')})`);
3387
3678
  for (let i = 0; i < keys.length; i++) {
@@ -3394,7 +3685,7 @@ export class ObjectsInRedisClient {
3394
3685
  this.log.error(`${this.namespace} Cannot parse JSON ${keys[i]}: ${objs[i]}`);
3395
3686
  continue;
3396
3687
  }
3397
- if (!utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
3688
+ if (!utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
3398
3689
  continue;
3399
3690
  }
3400
3691
  if (obj) {
@@ -3409,21 +3700,23 @@ export class ObjectsInRedisClient {
3409
3700
  // Calculate max
3410
3701
  if (func.reduce === '_stats') {
3411
3702
  let max = null;
3412
- for (let i = 0; i < result.rows.length; i++) {
3413
- if (max === null || result.rows[i].value > max) {
3414
- max = result.rows[i].value;
3703
+ for (let i = 0; i < rows.length; i++) {
3704
+ const _value = rows[i].value;
3705
+ if (max === null || (_value !== null && _value > max)) {
3706
+ max = rows[i].value;
3415
3707
  }
3416
3708
  }
3417
3709
  if (max !== null) {
3418
- result.rows = [{ id: '_stats', value: { max: max } }];
3710
+ // @ts-expect-error the _stats reduce result is not a regular object value
3711
+ rows = [{ id: '_stats', value: { max } }];
3419
3712
  }
3420
3713
  else {
3421
- result.rows = [];
3714
+ rows = [];
3422
3715
  }
3423
3716
  }
3424
- return result;
3717
+ return { rows: rows };
3425
3718
  }
3426
- async _getObjectView(design, search, params, options) {
3719
+ async _getObjectView(design, search, params, userContext) {
3427
3720
  if (!this.client) {
3428
3721
  throw new Error(ERRORS.ERROR_DB_CLOSED);
3429
3722
  }
@@ -3444,7 +3737,8 @@ export class ObjectsInRedisClient {
3444
3737
  throw new Error(`Cannot parse JSON: "_design/${design}" / "${obj}"`);
3445
3738
  }
3446
3739
  if (obj.views?.[search]) {
3447
- return this._applyViewFunc(obj.views[search], params, options);
3740
+ // @ts-expect-error missing typing
3741
+ return this._applyViewFunc(obj.views[search], params, userContext);
3448
3742
  }
3449
3743
  this.log.error(`${this.namespace} Cannot find search "${search}" in "${design}"`);
3450
3744
  throw new Error(`Cannot find search "${search}" in "${design}"`);
@@ -3454,6 +3748,15 @@ export class ObjectsInRedisClient {
3454
3748
  throw new Error(`Cannot find view "${design}"`);
3455
3749
  }
3456
3750
  }
3751
+ /**
3752
+ * Run a predefined object view (design document) and return the matching rows
3753
+ *
3754
+ * @param design The design document name
3755
+ * @param search The view name within the design document
3756
+ * @param params Query parameters such as startkey and endkey
3757
+ * @param options The current request options including the user, or the callback
3758
+ * @param callback Called with the matching rows
3759
+ */
3457
3760
  getObjectView(design, search, params, options, callback) {
3458
3761
  if (typeof options === 'function') {
3459
3762
  callback = options;
@@ -3462,16 +3765,13 @@ export class ObjectsInRedisClient {
3462
3765
  if (!callback) {
3463
3766
  return new Promise((resolve, reject) => this.getObjectView(design, search, params, options, (err, obj) => (err ? reject(err) : resolve(obj))));
3464
3767
  }
3465
- if (options?.acl) {
3466
- options.acl = null;
3467
- }
3468
3768
  if (typeof callback === 'function') {
3469
- utils.checkObjectRights(this, null, null, options, 'list', async (err, options) => {
3769
+ utils.checkObjectRights(this, null, null, options, 'list', async (err, userContext) => {
3470
3770
  if (err) {
3471
3771
  return tools.maybeCallbackWithRedisError(callback, err);
3472
3772
  }
3473
3773
  try {
3474
- const res = await this._getObjectView(design, search, params, options);
3774
+ const res = await this._getObjectView(design, search, params, userContext);
3475
3775
  return tools.maybeCallbackWithError(callback, null, res);
3476
3776
  }
3477
3777
  catch (e) {
@@ -3480,16 +3780,25 @@ export class ObjectsInRedisClient {
3480
3780
  });
3481
3781
  }
3482
3782
  }
3783
+ /**
3784
+ * Promise-version of getObjectView
3785
+ *
3786
+ * @param design The design document name
3787
+ * @param search The view name within the design document
3788
+ * @param params Query parameters such as startkey and endkey
3789
+ * @param options The current request options including the user
3790
+ * @param options.user The user on whose behalf the view is queried
3791
+ */
3483
3792
  getObjectViewAsync(design, search, params, options) {
3484
3793
  return this.getObjectView(design, search, params, options);
3485
3794
  }
3486
- async _getObjectList(params, options) {
3795
+ async _getObjectList(params, userContext) {
3487
3796
  if (!this.client) {
3488
3797
  throw new Error(ERRORS.ERROR_DB_CLOSED);
3489
3798
  }
3490
- params = params || {};
3491
- params.startkey = params.startkey || '';
3492
- params.endkey = params.endkey || '\u9999';
3799
+ params ||= {};
3800
+ params.startkey ||= '';
3801
+ params.endkey ||= '\u9999';
3493
3802
  const pattern = params.endkey.substring(0, params.startkey.length) === params.startkey
3494
3803
  ? `${this.objNamespace + params.startkey}*`
3495
3804
  : `${this.objNamespace}*`;
@@ -3538,7 +3847,7 @@ export class ObjectsInRedisClient {
3538
3847
  this.log.error(`${this.namespace} Cannot parse JSON ${_keys[i]}: ${objs[i]}`);
3539
3848
  continue;
3540
3849
  }
3541
- if (!obj || !utils.checkObject(obj, options, CONSTS.ACCESS_READ)) {
3850
+ if (!obj || !utils.checkObject(obj, userContext, CONSTS.ACCESS_READ)) {
3542
3851
  continue;
3543
3852
  }
3544
3853
  // @ts-expect-error TODO we are returning type Object for ease of use to devs, but formally these are AnyObjects, e.g. not guaranteed to have common
@@ -3547,6 +3856,13 @@ export class ObjectsInRedisClient {
3547
3856
  }
3548
3857
  return result;
3549
3858
  }
3859
+ /**
3860
+ * Get the list of objects matching the given parameters
3861
+ *
3862
+ * @param params Query parameters such as startkey and endkey
3863
+ * @param options The current request options including the user, or the callback
3864
+ * @param callback Called with the matching objects
3865
+ */
3550
3866
  getObjectList(params, options, callback) {
3551
3867
  if (typeof options === 'function') {
3552
3868
  callback = options;
@@ -3555,16 +3871,13 @@ export class ObjectsInRedisClient {
3555
3871
  if (!callback) {
3556
3872
  return new Promise((resolve, reject) => this.getObjectList(params, options, (err, obj) => (err ? reject(err) : resolve(obj))));
3557
3873
  }
3558
- if (options?.acl) {
3559
- options.acl = null;
3560
- }
3561
3874
  if (typeof callback === 'function') {
3562
- utils.checkObjectRights(this, null, null, options, 'list', async (err, options) => {
3875
+ utils.checkObjectRights(this, null, null, options, 'list', async (err, userContext) => {
3563
3876
  if (err) {
3564
3877
  return tools.maybeCallbackWithRedisError(callback, err);
3565
3878
  }
3566
3879
  try {
3567
- const res = await this._getObjectList(params, options || {});
3880
+ const res = await this._getObjectList(params, userContext);
3568
3881
  return tools.maybeCallbackWithError(callback, null, res);
3569
3882
  }
3570
3883
  catch (e) {
@@ -3573,35 +3886,34 @@ export class ObjectsInRedisClient {
3573
3886
  });
3574
3887
  }
3575
3888
  }
3889
+ /**
3890
+ * Promise-version of getObjectList
3891
+ *
3892
+ * @param params Query parameters such as startkey and endkey
3893
+ * @param options The current request options including the user
3894
+ */
3576
3895
  getObjectListAsync(params, options) {
3577
3896
  return this.getObjectList(params, options);
3578
3897
  }
3579
- // could be optimized, to read object only once. Now it will read 3 times
3580
- async _extendObject(id, obj, options, callback) {
3898
+ // could be optimized, to read an object only once. Now it will read 3 times
3899
+ async _extendObject(id, obj, options, userContext) {
3581
3900
  if (!id || typeof id !== 'string' || utils.REG_CHECK_ID.test(id)) {
3582
- // @ts-expect-error we fix when removing cb
3583
- return tools.maybeCallbackWithError(callback, `Invalid ID: ${id}`);
3901
+ throw new Error(`Invalid ID: ${id}`);
3584
3902
  }
3585
3903
  if (!this.client) {
3586
- return tools.maybeCallbackWithRedisError(callback, ERRORS.ERROR_DB_CLOSED);
3904
+ throw new Error(ERRORS.ERROR_DB_CLOSED);
3587
3905
  }
3906
+ const oldObjStr = await this.client.get(this.objNamespace + id);
3588
3907
  let oldObj;
3589
3908
  try {
3590
- oldObj = await this.client.get(this.objNamespace + id);
3591
- }
3592
- catch (e) {
3593
- return tools.maybeCallbackWithRedisError(callback, e);
3594
- }
3595
- try {
3596
- oldObj = oldObj ? JSON.parse(oldObj) : null;
3909
+ oldObj = oldObjStr ? JSON.parse(oldObjStr) : null;
3597
3910
  }
3598
3911
  catch {
3599
- this.log.error(`${this.namespace} Cannot parse JSON ${id}: ${oldObj}`);
3600
- // @ts-expect-error we fix when removing cb
3601
- return tools.maybeCallbackWithError(callback, `Cannot parse JSON ${id}: ${oldObj}`);
3912
+ this.log.error(`${this.namespace} Cannot parse JSON ${id}: ${oldObjStr}`);
3913
+ throw new Error(`Cannot parse JSON ${id}: ${oldObjStr}`);
3602
3914
  }
3603
- if (!utils.checkObject(oldObj, options, CONSTS.ACCESS_WRITE)) {
3604
- return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
3915
+ if (!utils.checkObject(oldObj, userContext, CONSTS.ACCESS_WRITE)) {
3916
+ throw new Error(ERRORS.ERROR_PERMISSION);
3605
3917
  }
3606
3918
  let _oldObj;
3607
3919
  if (oldObj?.nonEdit) {
@@ -3610,150 +3922,140 @@ export class ObjectsInRedisClient {
3610
3922
  }
3611
3923
  // we need to know if custom has been added/deleted
3612
3924
  const oldObjHasCustom = !!oldObj?.common?.custom;
3613
- oldObj = oldObj || {};
3614
- obj = deepClone(obj); // copy here to prevent "sandboxed" objects from JavaScript adapter
3615
- if (oldObj.common &&
3616
- oldObj.common.custom !== undefined &&
3617
- oldObj.common.custom !== null &&
3618
- !tools.isObject(oldObj.common.custom)) {
3925
+ // Clone the update here to prevent "sandboxed" objects from the JavaScript adapter
3926
+ const objClone = deepClone(obj);
3927
+ // Start from the existing object so the update is merged into it instead of replacing it
3928
+ let oldObjNotNull = oldObj || {};
3929
+ if (oldObjNotNull.common?.custom !== undefined &&
3930
+ oldObjNotNull.common.custom !== null &&
3931
+ !tools.isObject(oldObjNotNull.common.custom)) {
3619
3932
  // custom has to be an object, else clean up
3620
- delete oldObj.common.custom;
3933
+ delete oldObjNotNull.common.custom;
3621
3934
  }
3622
3935
  // we need to check if type has changed
3623
- const oldType = oldObj.type;
3624
- oldObj = extend(true, oldObj, obj);
3625
- oldObj._id = id;
3936
+ const oldType = oldObjNotNull.type;
3937
+ oldObjNotNull = extend(true, oldObjNotNull, objClone);
3938
+ oldObjNotNull._id = id;
3626
3939
  // add user default rights
3627
- if (this.defaultNewAcl && !oldObj.acl) {
3628
- oldObj.acl = deepClone(this.defaultNewAcl);
3629
- delete oldObj.acl.file;
3630
- if (oldObj.type !== 'state') {
3631
- delete oldObj.acl.state;
3940
+ if (this.defaultNewAcl && !oldObjNotNull.acl) {
3941
+ oldObjNotNull.acl = {
3942
+ owner: this.defaultNewAcl.owner,
3943
+ ownerGroup: this.defaultNewAcl.ownerGroup,
3944
+ object: this.defaultNewAcl.object,
3945
+ };
3946
+ if (oldObjNotNull.type === 'state') {
3947
+ oldObjNotNull.acl.state = this.defaultNewAcl.state;
3632
3948
  }
3633
3949
  if (options.owner) {
3634
- oldObj.acl.owner = options.owner;
3950
+ oldObjNotNull.acl.owner = options.owner;
3635
3951
  if (!options.ownerGroup) {
3636
- oldObj.acl.ownerGroup = null;
3637
- return void this.getUserGroup(options.owner, (user, groups /*, permissions */) => {
3638
- if (!groups || !groups[0]) {
3639
- options.ownerGroup = this.defaultNewAcl?.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP;
3640
- }
3641
- else {
3642
- options.ownerGroup = groups[0];
3643
- }
3644
- this._extendObject(id, obj, options, callback);
3645
- });
3952
+ const [, groups] = await utils.getUserGroupAsync(this, options.owner);
3953
+ if (!groups?.[0]) {
3954
+ options.ownerGroup = this.defaultNewAcl.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP;
3955
+ }
3956
+ else {
3957
+ options.ownerGroup = groups[0];
3958
+ }
3646
3959
  }
3647
3960
  }
3648
3961
  }
3649
- if (this.defaultNewAcl && options.ownerGroup && oldObj.acl && !oldObj.acl.ownerGroup) {
3650
- oldObj.acl.ownerGroup = options.ownerGroup;
3962
+ if (this.defaultNewAcl && options.ownerGroup && oldObjNotNull.acl && !oldObjNotNull.acl.ownerGroup) {
3963
+ oldObjNotNull.acl.ownerGroup = options.ownerGroup;
3651
3964
  }
3652
3965
  if (obj.common && 'alias' in obj.common && obj.common.alias.id) {
3653
3966
  if (typeof obj.common.alias.id === 'object') {
3654
3967
  if (typeof obj.common.alias.id.write !== 'string' || typeof obj.common.alias.id.read !== 'string') {
3655
- return tools.maybeCallbackWithError(callback, 'Invalid alias ID');
3968
+ throw new Error('Invalid alias ID');
3656
3969
  }
3657
3970
  if (obj.common.alias.id.write.startsWith('alias.') || obj.common.alias.id.read.startsWith('alias.')) {
3658
- return tools.maybeCallbackWithError(callback, 'Cannot make alias on alias');
3971
+ throw new Error('Cannot make alias on alias');
3659
3972
  }
3660
3973
  }
3661
3974
  else {
3662
3975
  if (typeof obj.common.alias.id !== 'string') {
3663
- return tools.maybeCallbackWithError(callback, 'Invalid alias ID');
3976
+ throw new Error('Invalid alias ID');
3664
3977
  }
3665
3978
  if (obj.common.alias.id.startsWith('alias.')) {
3666
- return tools.maybeCallbackWithError(callback, 'Cannot make alias on alias');
3979
+ throw new Error('Cannot make alias on alias');
3667
3980
  }
3668
3981
  }
3669
3982
  }
3670
- if (_oldObj && !tools.checkNonEditable(_oldObj, oldObj)) {
3671
- return tools.maybeCallbackWithError(callback, 'Invalid password for update of vendor information');
3983
+ if (_oldObj && !tools.checkNonEditable(_oldObj, oldObjNotNull)) {
3984
+ throw new Error('Invalid password for update of vendor information');
3672
3985
  }
3673
- const message = JSON.stringify(oldObj);
3674
- try {
3675
- const commands = [];
3676
- if (this.useSets) {
3677
- // what is called oldObj is acutally the obj we set, because it has been extended
3678
- if (oldObj.type && !oldType) {
3679
- // new object or oldObj had no type -> add to set + set object
3680
- commands.push(['sadd', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id]);
3681
- }
3682
- else if (oldObj.type && oldType && oldObj.type !== oldType) {
3683
- // the old obj had a type which differs from the new type -> rem old, add new
3684
- commands.push(['sadd', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id], ['srem', `${this.setNamespace}object.type.${oldObj.type}`, this.objNamespace + id]);
3685
- }
3686
- else if (oldType && !oldObj.type) {
3687
- // the oldObj had a type, the new one has no -> rem
3688
- commands.push(['srem', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id]);
3689
- }
3690
- }
3691
- if (oldObj.common?.custom && !oldObjHasCustom) {
3692
- // we now have custom, old object had no custom
3693
- commands.push(['sadd', `${this.setNamespace}object.common.custom`, this.objNamespace + id]);
3694
- }
3695
- else if (oldObjHasCustom && (!oldObj.common || !oldObj.common.custom)) {
3696
- // we no longer have custom
3697
- commands.push(['srem', `${this.setNamespace}object.common.custom`, this.objNamespace + id]);
3698
- }
3699
- if (!commands.length) {
3700
- await this.client.set(this.objNamespace + id, message);
3986
+ const message = JSON.stringify(oldObjNotNull);
3987
+ const commands = [];
3988
+ if (this.useSets) {
3989
+ // what is called oldObjNotNull is actually the obj we set, because it has been extended
3990
+ if (oldObjNotNull.type && !oldType) {
3991
+ // new object or oldObjNotNull had no type -> add to set + set object
3992
+ commands.push(['sadd', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id]);
3701
3993
  }
3702
- else {
3703
- // set all commands atomic
3704
- commands.push(['set', this.objNamespace + id, message]);
3705
- await this.client.multi(commands).exec();
3994
+ else if (oldObjNotNull.type && oldType && oldObjNotNull.type !== oldType) {
3995
+ // the old obj had a type which differs from the new type -> rem old, add new
3996
+ commands.push(['sadd', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id], ['srem', `${this.setNamespace}object.type.${oldObjNotNull.type}`, this.objNamespace + id]);
3706
3997
  }
3707
- // extended -> if its now type meta and currently marked as not -> cache
3708
- if (this.existingMetaObjects[id] === false && oldObj && oldObj.type === 'meta') {
3709
- this.existingMetaObjects[id] = true;
3998
+ else if (oldType && !oldObjNotNull.type) {
3999
+ // the oldObjNotNull had a type, the new one has no -> rem
4000
+ commands.push(['srem', `${this.setNamespace}object.type.${obj.type}`, this.objNamespace + id]);
3710
4001
  }
3711
- await this.client.publish(this.objNamespace + id, message);
3712
- return tools.maybeCallbackWithError(callback, null, { id: id, value: oldObj }, id);
3713
4002
  }
3714
- catch (e) {
3715
- return tools.maybeCallbackWithRedisError(callback, e);
4003
+ if (oldObjNotNull.common?.custom && !oldObjHasCustom) {
4004
+ // we now have custom, old object had no custom
4005
+ commands.push(['sadd', `${this.setNamespace}object.common.custom`, this.objNamespace + id]);
3716
4006
  }
3717
- }
3718
- extendObject(id, obj, options, callback) {
3719
- if (typeof options === 'function') {
3720
- callback = options;
3721
- options = null;
4007
+ else if (oldObjHasCustom && (!oldObjNotNull.common || !oldObjNotNull.common.custom)) {
4008
+ // we no longer have custom
4009
+ commands.push(['srem', `${this.setNamespace}object.common.custom`, this.objNamespace + id]);
3722
4010
  }
3723
- if (!callback) {
3724
- return new Promise((resolve, reject) => this.extendObject(id, obj, options, (err, res) => (err ? reject(err) : resolve(res))));
4011
+ if (!commands.length) {
4012
+ await this.client.set(this.objNamespace + id, message);
3725
4013
  }
3726
- if (options?.acl) {
3727
- options.acl = null;
4014
+ else {
4015
+ // set all commands atomic
4016
+ commands.push(['set', this.objNamespace + id, message]);
4017
+ await this.client.multi(commands).exec();
3728
4018
  }
3729
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, options) => {
3730
- if (err) {
3731
- return tools.maybeCallbackWithRedisError(callback, err);
3732
- }
3733
- // @ts-expect-error TODO we are returning type Object for ease of use to devs, but formally these are AnyObjects, e.g. not guaranteed to have common
3734
- return this._extendObject(id, obj, options, callback);
3735
- });
4019
+ // extended -> if it's now type meta and currently marked as not -> cache
4020
+ if (this.existingMetaObjects[id] === false && oldObjNotNull?.type === 'meta') {
4021
+ this.existingMetaObjects[id] = true;
4022
+ }
4023
+ await this.client.publish(this.objNamespace + id, message);
4024
+ return { id, value: oldObjNotNull };
3736
4025
  }
3737
- extendObjectAsync(id, obj, options) {
3738
- return new Promise((resolve, reject) => this.extendObject(id, obj, options || null, (err, res) => (err ? reject(err) : resolve(res))));
4026
+ /**
4027
+ * Extend an existing object with the given partial object, creating it if it does not exist
4028
+ *
4029
+ * @param id The id of the object to extend
4030
+ * @param obj The partial object to merge into the existing object
4031
+ * @param options The current request options including the user, or the callback
4032
+ */
4033
+ async extendObject(id, obj, options) {
4034
+ const userContext = await checkObjectRightsAsync(this, null, null, options, CONSTS.ACCESS_WRITE);
4035
+ return this._extendObject(id, obj, options || {}, userContext);
3739
4036
  }
3740
4037
  /**
3741
4038
  * Returns the object id if found
3742
4039
  *
3743
- * @param idOrName
3744
- * @param type
3745
- * @param options
3746
- * @param callback
4040
+ * @param idOrName The id or name to search for
4041
+ * @param type The expected common type, or null for any
4042
+ * @param options The current request options (may include a language)
4043
+ * @param options.language The language to use when resolving names
4044
+ * @param options.user The user on whose behalf the lookup is performed
4045
+ * @param userContext The resolved user context to check the rights against
4046
+ * @param callback Called with the found id and the original id/name
3747
4047
  */
3748
- _findObject(idOrName, type, options, callback) {
4048
+ _findObject(idOrName, type, options, userContext, callback) {
3749
4049
  // Try to read by ID
3750
- this._getObject(idOrName, options, (err, obj) => {
4050
+ this._getObject(idOrName, userContext, (err, obj) => {
3751
4051
  // Assume it is ID
3752
- if (obj && utils.checkObject(obj, options, CONSTS.ACCESS_READ) && (!type || obj.common?.type === type)) {
4052
+ if (obj &&
4053
+ utils.checkObject(obj, userContext, CONSTS.ACCESS_READ) &&
4054
+ (!type || obj.common?.type === type)) {
3753
4055
  return tools.maybeCallbackWithError(callback, null, idOrName, obj.common.name);
3754
4056
  }
3755
4057
  // Get all objects that this user may read
3756
- this._getKeys('*', options, async (err, keys) => {
4058
+ return this._getKeys('*', userContext, true, async (err, keys) => {
3757
4059
  if (!this.client) {
3758
4060
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);
3759
4061
  }
@@ -3790,9 +4092,17 @@ export class ObjectsInRedisClient {
3790
4092
  }
3791
4093
  }
3792
4094
  return tools.maybeCallbackWithError(callback, null, undefined, idOrName);
3793
- }, true);
3794
- });
4095
+ });
4096
+ }).catch(e => this.log.error(`${this.namespace} Cannot find object: ${e.message}`));
3795
4097
  }
4098
+ /**
4099
+ * Find an object by its id or name
4100
+ *
4101
+ * @param idOrName The id or name to search for
4102
+ * @param type The expected common type, or null for any
4103
+ * @param options The current request options (may include a language), or the callback
4104
+ * @param callback Called with the found id and the original id/name
4105
+ */
3796
4106
  findObject(idOrName, type, options, callback) {
3797
4107
  if (typeof type === 'function') {
3798
4108
  callback = type;
@@ -3806,19 +4116,21 @@ export class ObjectsInRedisClient {
3806
4116
  if (!callback) {
3807
4117
  return new Promise((resolve, reject) => this.findObject(idOrName, type, options, (err, id, _idOrName) => (err ? reject(err) : resolve(id))));
3808
4118
  }
3809
- if (options?.acl) {
3810
- options.acl = null;
3811
- }
3812
4119
  if (typeof callback === 'function') {
3813
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_LIST, (err, options) => {
4120
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_LIST, (err, userContext) => {
3814
4121
  if (err) {
3815
4122
  return tools.maybeCallbackWithError(callback, err);
3816
4123
  }
3817
- return this._findObject(idOrName, type, options, callback);
4124
+ return this._findObject(idOrName, type, options, userContext, callback);
3818
4125
  });
3819
4126
  }
3820
4127
  }
3821
4128
  // can be called only from js-controller
4129
+ /**
4130
+ * Add object property paths that should be preserved when an object is overwritten (controller only)
4131
+ *
4132
+ * @param settings One or more property paths to preserve
4133
+ */
3822
4134
  addPreserveSettings(settings) {
3823
4135
  if (!Array.isArray(settings)) {
3824
4136
  settings = [settings];
@@ -3860,26 +4172,40 @@ export class ObjectsInRedisClient {
3860
4172
  return tools.maybeCallbackWithRedisError(callback, e);
3861
4173
  }
3862
4174
  }
4175
+ /**
4176
+ * Delete the whole objects database (requires admin rights)
4177
+ *
4178
+ * @param options The current request options including the user, or the callback
4179
+ * @param callback Called once the database has been destroyed
4180
+ */
3863
4181
  destroyDB(options, callback) {
3864
4182
  if (typeof options === 'function') {
3865
4183
  callback = options;
3866
4184
  options = null;
3867
4185
  }
3868
4186
  options = options || {};
3869
- utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, options) => {
4187
+ utils.checkObjectRights(this, null, null, options, CONSTS.ACCESS_WRITE, (err, userContext) => {
3870
4188
  if (err) {
3871
4189
  return tools.maybeCallbackWithRedisError(callback, err);
3872
4190
  }
3873
- if (!options.acl.file.write || options.user !== CONSTS.SYSTEM_ADMIN_USER) {
4191
+ if (!userContext.acl.file.write || userContext.user !== CONSTS.SYSTEM_ADMIN_USER) {
3874
4192
  return tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION);
3875
4193
  }
3876
4194
  return this._destroyDB(callback);
3877
4195
  });
3878
4196
  }
4197
+ /**
4198
+ * Promise-version of destroyDB
4199
+ *
4200
+ * @param options The current request options including the user
4201
+ * @param options.user The user on whose behalf the database is destroyed
4202
+ */
3879
4203
  destroyDBAsync(options) {
3880
4204
  return new Promise((resolve, reject) => this.destroyDB(options, err => (err ? reject(err) : resolve())));
3881
4205
  }
3882
- // Destructor of the class. Called by shutting down.
4206
+ /**
4207
+ * Destructor of the class. Called when shutting down to close the redis connections.
4208
+ */
3883
4209
  async destroy() {
3884
4210
  this.stop = true;
3885
4211
  if (this.client) {
@@ -3913,6 +4239,9 @@ export class ObjectsInRedisClient {
3913
4239
  }
3914
4240
  }
3915
4241
  }
4242
+ /**
4243
+ * Load and register the Lua scripts used for atomic operations on the redis server
4244
+ */
3916
4245
  async loadLuaScripts() {
3917
4246
  let luaDirName;
3918
4247
  if (this.noLegacyMultihost && this.useSets) {
@@ -3933,15 +4262,13 @@ export class ObjectsInRedisClient {
3933
4262
  return { name: name.replace(/\.lua$/, ''), text: script, hash };
3934
4263
  });
3935
4264
  const hashes = scripts.map(e => e.hash);
3936
- hashes.unshift('EXISTS');
3937
4265
  if (!this.client) {
3938
4266
  throw new Error(tools.ERRORS.ERROR_DB_CLOSED);
3939
4267
  }
3940
- let arr;
3941
4268
  try {
3942
- arr = await this.client.script(hashes);
4269
+ const arr = (await this.client.script('EXISTS', ...hashes));
3943
4270
  if (arr) {
3944
- scripts.forEach((e, i) => (scripts[i].loaded = !!arr[i]));
4271
+ scripts.forEach((_e, i) => (scripts[i].loaded = !!arr[i]));
3945
4272
  }
3946
4273
  }
3947
4274
  catch {
@@ -3954,7 +4281,7 @@ export class ObjectsInRedisClient {
3954
4281
  if (!script.loaded) {
3955
4282
  let hash;
3956
4283
  try {
3957
- hash = await this.client.script(['LOAD', script.text]);
4284
+ hash = (await this.client.script('LOAD', script.text));
3958
4285
  script.loaded = true;
3959
4286
  }
3960
4287
  catch (e) {
@@ -3966,16 +4293,16 @@ export class ObjectsInRedisClient {
3966
4293
  throw new Error(`Cannot load "${script.name}" into objects database: ${e.message}`);
3967
4294
  }
3968
4295
  }
3969
- script.hash = hash;
4296
+ script.hash = hash || '';
3970
4297
  }
3971
4298
  }
3972
4299
  this.scripts = {};
3973
4300
  scripts.forEach(e => (this.scripts[e.name] = e.hash));
3974
4301
  }
3975
4302
  /**
3976
- * Get all keys matching a pattern using redis SCAN command, duplicates are filtered out
4303
+ * Get all keys matching a pattern using redis SCAN command; duplicates are filtered out
3977
4304
  *
3978
- * @param pattern - pattern to match, e. g. io.hm-rpc.0*
4305
+ * @param pattern - pattern to match, e.g. io.hm-rpc.0*
3979
4306
  * @param count - count argument used by redis SCAN, default is 250
3980
4307
  */
3981
4308
  _getKeysViaScan(pattern, count = 250) {
@@ -3997,6 +4324,7 @@ export class ObjectsInRedisClient {
3997
4324
  // return without duplicates
3998
4325
  resolve(Array.from(new Set(uniqueKeys)));
3999
4326
  });
4327
+ stream.on('error', reject);
4000
4328
  });
4001
4329
  }
4002
4330
  /**
@@ -4023,7 +4351,7 @@ export class ObjectsInRedisClient {
4023
4351
  if (!this.client) {
4024
4352
  throw new Error(ERRORS.ERROR_DB_CLOSED);
4025
4353
  }
4026
- // to be safe we remove all sets before migration
4354
+ // to be safe, we remove all sets before migration
4027
4355
  const keys = await this._getKeysViaScan(`${this.setNamespace}object.type.*`);
4028
4356
  for (const key of keys) {
4029
4357
  await this.client.del(key);
@@ -4074,17 +4402,11 @@ export class ObjectsInRedisClient {
4074
4402
  return Promise.resolve(0);
4075
4403
  }
4076
4404
  // try to extend lock
4077
- return this.client.evalsha([
4078
- this.scripts.redlock_extend,
4079
- 3,
4080
- `${this.metaNamespace}objects.primaryHost`,
4081
- this.hostname,
4082
- ms,
4083
- ]);
4405
+ return this.client.evalsha(this.scripts.redlock_extend, 3, `${this.metaNamespace}objects.primaryHost`, this.hostname, ms);
4084
4406
  }
4085
4407
  /**
4086
- * Sets current host as primary if no primary host active
4087
- * Value will expire after ms milliseconds
4408
+ * Sets the current host as primary if no primary host active
4409
+ * Value expires after ms milliseconds
4088
4410
  *
4089
4411
  * @param ms - ms until value expires
4090
4412
  * @returns 1 if lock acquired else 0
@@ -4098,16 +4420,10 @@ export class ObjectsInRedisClient {
4098
4420
  return Promise.resolve(0);
4099
4421
  }
4100
4422
  // try to acquire lock
4101
- return this.client.evalsha([
4102
- this.scripts.redlock_acquire,
4103
- 3,
4104
- `${this.metaNamespace}objects.primaryHost`,
4105
- this.hostname,
4106
- ms,
4107
- ]);
4423
+ return this.client.evalsha(this.scripts.redlock_acquire, 3, `${this.metaNamespace}objects.primaryHost`, this.hostname, ms);
4108
4424
  }
4109
4425
  /**
4110
- * Get name of the primary host
4426
+ * Get the name of the primary host
4111
4427
  */
4112
4428
  getPrimaryHost() {
4113
4429
  if (!this.client) {
@@ -4130,14 +4446,7 @@ export class ObjectsInRedisClient {
4130
4446
  return Promise.resolve();
4131
4447
  }
4132
4448
  // try to release lock
4133
- return this.client.evalsha([
4134
- this.scripts.redlock_release,
4135
- 4,
4136
- `${this.metaNamespace}objects.primaryHost`,
4137
- this.hostname,
4138
- this.settings.connection.options.db,
4139
- `${this.metaNamespace}objects.primaryHost`,
4140
- ]);
4449
+ return this.client.evalsha(this.scripts.redlock_release, 4, `${this.metaNamespace}objects.primaryHost`, this.hostname, this.settings.connection.options.db || 0, `${this.metaNamespace}objects.primaryHost`);
4141
4450
  }
4142
4451
  /**
4143
4452
  * Sets the protocol version to the DB
@@ -4159,7 +4468,7 @@ export class ObjectsInRedisClient {
4159
4468
  }
4160
4469
  }
4161
4470
  /**
4162
- * Subscribe to expired events to get expiration of primary host
4471
+ * Subscribe to expired events to get expiration of the primary host
4163
4472
  */
4164
4473
  async subscribePrimaryHost() {
4165
4474
  if (this.subSystem) {