roboto-js 1.9.15 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -351,11 +351,47 @@ var RbtObject = /*#__PURE__*/function () {
351
351
  });
352
352
  return clonedObject;
353
353
  }
354
+
355
+ /**
356
+ * Rebuild read_ids/write_ids record meta from iac grant arrays.
357
+ * Keeps denormalized columns in sync with dataJson.iac for delta saves.
358
+ */
359
+ }, {
360
+ key: "_syncIacRecordMeta",
361
+ value: function _syncIacRecordMeta() {
362
+ var iac = _.get(this._data, 'iac');
363
+ if (!iac) return;
364
+ var readIds = new Set();
365
+ var writeIds = new Set();
366
+ if (this.type === '<@iac.user>') {
367
+ writeIds.add(this.id);
368
+ readIds.add(this.id);
369
+ }
370
+ if (iac.creator) {
371
+ writeIds.add(iac.creator);
372
+ readIds.add(iac.creator);
373
+ }
374
+ var collect = function collect(grants, target) {
375
+ if (!grants) return;
376
+ for (var _i = 0, _arr = ['users', 'userGroups', 'organizations', 'userSegments']; _i < _arr.length; _i++) {
377
+ var key = _arr[_i];
378
+ if (Array.isArray(grants[key])) {
379
+ grants[key].forEach(function (id) {
380
+ return target.add(id);
381
+ });
382
+ }
383
+ }
384
+ };
385
+ collect(iac.readGrants, readIds);
386
+ collect(iac.writeGrants, writeIds);
387
+ this._internalData.read_ids = Array.from(readIds).join(',');
388
+ this._internalData.write_ids = Array.from(writeIds).join(',');
389
+ }
354
390
  }, {
355
391
  key: "save",
356
392
  value: function () {
357
393
  var _save = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3() {
358
- var _response$data, _response$data2, record, response;
394
+ var _response$data, _response$data2, iacGrantChange, record, response;
359
395
  return _regeneratorRuntime().wrap(function _callee3$(_context3) {
360
396
  while (1) switch (_context3.prev = _context3.next) {
361
397
  case 0:
@@ -366,28 +402,34 @@ var RbtObject = /*#__PURE__*/function () {
366
402
  throw new Error('Cannot save object without type');
367
403
  case 2:
368
404
  _context3.prev = 2;
405
+ iacGrantChange = (this.rpcMeta.changes || []).some(function (path) {
406
+ return path.startsWith('iac.readGrants') || path.startsWith('iac.writeGrants');
407
+ });
408
+ if (iacGrantChange) {
409
+ this._syncIacRecordMeta();
410
+ }
369
411
  if (this.rpcMeta.isNew) {
370
412
  record = this.toRecord();
371
413
  } else {
372
414
  record = this.toDeltaRecord();
373
415
  }
374
- _context3.next = 6;
416
+ _context3.next = 8;
375
417
  return this._axios.post('/object_service/saveObject', [record]);
376
- case 6:
418
+ case 8:
377
419
  response = _context3.sent;
378
420
  if (!(response.data.ok === false)) {
379
- _context3.next = 9;
421
+ _context3.next = 11;
380
422
  break;
381
423
  }
382
424
  throw new Error(response.data.message);
383
- case 9:
425
+ case 11:
384
426
  if (!(((_response$data = response.data) === null || _response$data === void 0 ? void 0 : _response$data.result) == 'NO_CHANGES')) {
385
- _context3.next = 12;
427
+ _context3.next = 14;
386
428
  break;
387
429
  }
388
430
  this.rpcMeta.isNew = false;
389
431
  return _context3.abrupt("return", this);
390
- case 12:
432
+ case 14:
391
433
  if ((_response$data2 = response.data) !== null && _response$data2 !== void 0 && _response$data2.newData) {
392
434
  // apply new incoming data
393
435
  this.setData(response.newData);
@@ -397,17 +439,17 @@ var RbtObject = /*#__PURE__*/function () {
397
439
  this.type = response.data.type;
398
440
  this.rpcMeta.isNew = false;
399
441
  return _context3.abrupt("return", this);
400
- case 20:
401
- _context3.prev = 20;
442
+ case 22:
443
+ _context3.prev = 22;
402
444
  _context3.t0 = _context3["catch"](2);
403
445
  this._error = _context3.t0;
404
446
  //console.log(e.response.data);
405
447
  throw _context3.t0;
406
- case 24:
448
+ case 26:
407
449
  case "end":
408
450
  return _context3.stop();
409
451
  }
410
- }, _callee3, this, [[2, 20]]);
452
+ }, _callee3, this, [[2, 22]]);
411
453
  }));
412
454
  function save() {
413
455
  return _save.apply(this, arguments);
@@ -524,19 +566,20 @@ var RbtObject = /*#__PURE__*/function () {
524
566
  this.set(groupsPath, _toConsumableArray(new Set([].concat(_toConsumableArray(existingGroups), _toConsumableArray(groupIds)))));
525
567
  }
526
568
  }
569
+ this._syncIacRecordMeta();
527
570
 
528
571
  // Save if requested
529
572
  if (!save) {
530
- _context4.next = 15;
573
+ _context4.next = 16;
531
574
  break;
532
575
  }
533
- _context4.next = 14;
576
+ _context4.next = 15;
534
577
  return this.save();
535
- case 14:
536
- return _context4.abrupt("return", _context4.sent);
537
578
  case 15:
538
- return _context4.abrupt("return", this);
579
+ return _context4.abrupt("return", _context4.sent);
539
580
  case 16:
581
+ return _context4.abrupt("return", this);
582
+ case 17:
540
583
  case "end":
541
584
  return _context4.stop();
542
585
  }
@@ -769,19 +812,20 @@ var RbtObject = /*#__PURE__*/function () {
769
812
  return !groupIds.includes(id);
770
813
  }));
771
814
  }
815
+ this._syncIacRecordMeta();
772
816
 
773
817
  // Save if requested
774
818
  if (!save) {
775
- _context7.next = 15;
819
+ _context7.next = 16;
776
820
  break;
777
821
  }
778
- _context7.next = 14;
822
+ _context7.next = 15;
779
823
  return this.save();
780
- case 14:
781
- return _context7.abrupt("return", _context7.sent);
782
824
  case 15:
783
- return _context7.abrupt("return", this);
825
+ return _context7.abrupt("return", _context7.sent);
784
826
  case 16:
827
+ return _context7.abrupt("return", this);
828
+ case 17:
785
829
  case "end":
786
830
  return _context7.stop();
787
831
  }
@@ -151,7 +151,7 @@ var RbtUser = /*#__PURE__*/function () {
151
151
  record = this.toRecord();
152
152
  record.type = '<@iac.user>';
153
153
  _context.next = 6;
154
- return this._axios.post('/user_service/saveUser', [record]);
154
+ return this._axios.post('/api/iac/saveUser', [record]);
155
155
  case 6:
156
156
  response = _context.sent;
157
157
  if (!(response.data.ok === false)) {
@@ -1,4 +1,4 @@
1
1
  // Auto-generated version file
2
2
  // DO NOT EDIT - This file is automatically updated from package.json
3
- // Version: 1.9.15
4
- export var version = '1.9.15';
3
+ // Version: 3.0.1
4
+ export var version = '3.0.1';
@@ -1124,7 +1124,8 @@ var RbtObject = /*#__PURE__*/function () {
1124
1124
  // Continue to next handler instead of breaking the loop
1125
1125
  }
1126
1126
  }
1127
- console.log("[AgentProviderSync] \u2713 Completed calling ".concat(handlers.length, " handlers"));
1127
+
1128
+ // ✓ Completed calling ${handlers.length} handlers`);
1128
1129
  }
1129
1130
  }, {
1130
1131
  key: "_broadcastChange",
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Auto-generated version file
2
2
  // DO NOT EDIT - This file is automatically updated from package.json
3
- // Version: 1.9.14
4
- export var version = '1.9.14';
3
+ // Version: 1.9.15
4
+ export var version = '1.9.15';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roboto-js",
3
- "version": "1.9.15",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "main": "dist/cjs/index.cjs",
package/src/index.js CHANGED
@@ -298,10 +298,18 @@ export default class Roboto{
298
298
  async confirmUserEmail(params){
299
299
  return this.api.confirmUserEmail(params);
300
300
  }
301
-
302
- //
303
- // Organization management
304
- //
301
+ async sendResetEmailViaFlows(email) {
302
+ return this.api.sendResetEmailViaFlows(email);
303
+ }
304
+ async sendRegistrationVerificationEmail(email) {
305
+ return this.api.sendRegistrationVerificationEmail(email);
306
+ }
307
+ async resendRegistrationCode(email) {
308
+ return this.api.resendRegistrationCode(email);
309
+ }
310
+ async completePasswordReset(params) {
311
+ return this.api.completePasswordReset(params);
312
+ }
305
313
  async loadCurrentOrganization(forceReload = false){
306
314
  return this.api.loadCurrentOrganization(forceReload);
307
315
  }
@@ -315,7 +323,7 @@ export default class Roboto{
315
323
  return this.api.getCurrentOrganization();
316
324
  }
317
325
  get currentOrganization(){
318
- return this.api.currentOrganization;
326
+ return this.api.getCurrentOrganization();
319
327
  }
320
328
 
321
329
  //
package/src/rbt_api.js CHANGED
@@ -6,6 +6,32 @@ import RbtFile from './rbt_file.js';
6
6
  import _ from 'lodash';
7
7
  import { openDB } from 'idb';
8
8
 
9
+ /** IAC working org preference lives under module-scoped settings (`mod.iac`). Legacy reads still honor `mod.currentOrgId`. */
10
+ const USER_MOD_CURRENT_ORG_IAC = 'mod.iac.currentOrgId';
11
+ const USER_MOD_CURRENT_ORG_LEGACY = 'mod.currentOrgId';
12
+
13
+ /** @param {import('./rbt_user.js').default|null|undefined} user */
14
+ function readUserCurrentOrgPreference(user) {
15
+ if (!user || typeof user.get !== 'function') return '';
16
+
17
+ let v = user.get(USER_MOD_CURRENT_ORG_IAC);
18
+ if (v != null && String(v).trim() !== '') return String(v).trim();
19
+ v = user.get(USER_MOD_CURRENT_ORG_LEGACY);
20
+ if (v != null && String(v).trim() !== '') return String(v).trim();
21
+
22
+ const d = typeof user.getData === 'function' ? user.getData() : {};
23
+ v = _.get(d, 'mod.iac.currentOrgId') ?? _.get(d, 'mod.currentOrgId');
24
+ if (v != null && String(v).trim() !== '') return String(v).trim();
25
+
26
+ return '';
27
+ }
28
+
29
+ /** @param {import('./rbt_user.js').default} user */
30
+ function persistUserCurrentOrgPreference(user, orgId) {
31
+ if (!user?.set || orgId == null || String(orgId).trim() === '') return;
32
+ user.set(USER_MOD_CURRENT_ORG_IAC, String(orgId).trim());
33
+ }
34
+
9
35
  export default class RbtApi {
10
36
 
11
37
  constructor({ baseUrl, accesskey, authtoken=null, apikey=null, localStorageAdaptor=null, withCredentials=true }) {
@@ -299,7 +325,7 @@ export default class RbtApi {
299
325
 
300
326
  try {
301
327
 
302
- const response = await this.axios.post('/user_service/loginUser', [{
328
+ const response = await this.axios.post('/api/iac/loginUser', [{
303
329
  email: params.email,
304
330
  password: (params.password)? CryptoJS.MD5(params.password).toString(CryptoJS.enc.Hex): null, // legacy hash password (md5)
305
331
  passwd: params.password || null, // current password
@@ -378,9 +404,9 @@ export default class RbtApi {
378
404
 
379
405
  async logout() {
380
406
  try {
381
- // Call logout endpoint if necessary. Here, I assume there's a '/user_service/logoutUser' endpoint.
407
+ // Call logout endpoint if necessary. Here, I assume there's a '/api/iac/logoutUser' endpoint.
382
408
  // This is optional and depends on how your backend is set up.
383
- const response = await this.axios.post('/user_service/logoutUser');
409
+ const response = await this.axios.post('/api/iac/logoutUser');
384
410
 
385
411
  if (response.data.ok === false) {
386
412
  return this._handleError(response);
@@ -471,24 +497,64 @@ export default class RbtApi {
471
497
  }
472
498
 
473
499
 
474
- async confirmUserEmail(confirmCode){
475
-
476
- let params = { emailConfirmCode: confirmCode };
500
+ /**
501
+ * Confirm email with either legacy composite string or { email, code } (6-digit from mod-iac-flows).
502
+ * @param {string|{ email: string, code: string }|{ emailConfirmCode: string }} confirmCodeOrParams
503
+ */
504
+ async confirmUserEmail(confirmCodeOrParams) {
505
+ let params;
506
+ if (typeof confirmCodeOrParams === 'string') {
507
+ params = { emailConfirmCode: confirmCodeOrParams };
508
+ } else if (confirmCodeOrParams && typeof confirmCodeOrParams === 'object') {
509
+ const { email, code, emailConfirmCode } = confirmCodeOrParams;
510
+ if (email && code != null && String(code).length > 0) {
511
+ params = { email, code: String(code) };
512
+ } else if (emailConfirmCode) {
513
+ params = { emailConfirmCode };
514
+ } else {
515
+ const err = new Error('confirmUserEmail: pass a legacy string, { emailConfirmCode }, or { email, code }');
516
+ return this._handleError(err);
517
+ }
518
+ } else {
519
+ const err = new Error('confirmUserEmail: invalid argument');
520
+ return this._handleError(err);
521
+ }
477
522
 
478
523
  try {
479
-
480
- const response = await this.axios.post('/user_service/confirmUserEmail', [params]);
481
-
524
+ const response = await this.axios.post('/api/iac/confirmUserEmail', [params]);
482
525
  if (response.data.ok === false) {
483
526
  return this._handleError(response);
484
527
  }
485
-
486
528
  return response.data;
487
-
488
529
  } catch (e) {
489
530
  return this._handleError(e);
490
531
  }
532
+ }
533
+
534
+ /** Password reset email — use mod-iac-flows (not deprecated /api/iac/sendPasswordReset). */
535
+ async sendResetEmailViaFlows(email) {
536
+ const response = await this.axios.post('/api/iac-flows/sendResetEmail', { email });
537
+ return response.data;
538
+ }
491
539
 
540
+ async sendRegistrationVerificationEmail(email) {
541
+ const response = await this.axios.post('/api/iac-flows/sendRegistrationVerification', { email });
542
+ return response.data;
543
+ }
544
+
545
+ async resendRegistrationCode(email) {
546
+ const response = await this.axios.post('/api/iac-flows/resendRegistrationCode', { email });
547
+ return response.data;
548
+ }
549
+
550
+ async completePasswordReset({ email, code, newPassword }) {
551
+ const response = await this.axios.post('/api/iac/completePasswordReset', [
552
+ { email, code, newPassword },
553
+ ]);
554
+ if (response.data?.ok === false) {
555
+ return this._handleError(response);
556
+ }
557
+ return response.data;
492
558
  }
493
559
 
494
560
  async loadCurrentUserExtended(){
@@ -516,55 +582,55 @@ export default class RbtApi {
516
582
  }
517
583
 
518
584
  /**
519
- * Load current organization for the authenticated user
520
- * Organization is determined by:
521
- * 1. User's mod.currentOrgId preference
522
- * 2. First organization in user.organizations array
523
- * 3. null if user has no organizations
524
- *
525
- * @param {boolean} forceReload - Force reload from server even if cached
585
+ * Load current organization for the authenticated user.
586
+ *
587
+ * Returns an organization **only when** `user.mod.iac.currentOrgId` is set (explicit selection via
588
+ * `switchOrganization` / `selectCurrentOrganization`; legacy reads also accept `mod.currentOrgId`).
589
+ * There is no fallback to “first org”; callers must guide the user to select an org first.
590
+ *
591
+ * @param {boolean} forceReload - Force reload from server even if cached ID matches preference
526
592
  * @returns {Promise<RbtObject|null>} Organization object or null
527
593
  */
528
594
  async loadCurrentOrganization(forceReload = false) {
529
595
  try {
530
- // Return cached if available and not forcing reload
531
- if (this.currentOrganization && !forceReload) {
532
- //console.log('[RbtApi] Returning cached currentOrganization:', this.currentOrganization.get('name'));
533
- return this.currentOrganization;
534
- }
535
-
536
596
  // Prevent duplicate concurrent loads
537
597
  if (this._loadCurrentOrgPromise) {
538
- //console.log('[RbtApi] Organization load already in progress');
539
598
  return this._loadCurrentOrgPromise;
540
599
  }
541
600
 
542
601
  this._loadCurrentOrgPromise = (async () => {
543
602
  try {
544
- // Ensure user is loaded first
545
603
  const user = await this.loadCurrentUser();
546
604
  if (!user) {
547
- //console.log('[RbtApi] No current user, cannot load organization');
605
+ this.currentOrganization = null;
548
606
  return null;
549
607
  }
550
608
 
551
- // Get organization ID from user preferences or first org
552
- const userData = user.getData();
553
- // organizations array format: [{ id: 'org123', roles: ['owner'] }, ...]
554
- const firstOrg = userData.organizations?.[0];
555
- const orgId = userData.mod?.currentOrgId || (typeof firstOrg === 'object' ? firstOrg?.id : firstOrg);
556
-
609
+ const orgId = readUserCurrentOrgPreference(user);
610
+
557
611
  if (!orgId) {
558
- //console.log('[RbtApi] User has no organizations');
612
+ //console.log('[RbtApi] No current organization selected (mod.iac.currentOrgId unset)');
613
+ this.currentOrganization = null;
614
+ if (this.localStorageAdaptor) {
615
+ try {
616
+ await this.localStorageAdaptor.removeItem('currentOrgId');
617
+ } catch (_) {
618
+ /* noop */
619
+ }
620
+ }
559
621
  return null;
560
622
  }
561
-
562
- console.log('[RbtApi] Resolved organization ID:', orgId, 'from:', {
563
- modCurrentOrgId: userData.mod?.currentOrgId,
564
- firstOrg: firstOrg
565
- });
566
623
 
567
- // Check if we have a cached org ID and it matches
624
+ if (
625
+ !forceReload &&
626
+ this.currentOrganization &&
627
+ String(this.currentOrganization.id) === orgId
628
+ ) {
629
+ return this.currentOrganization;
630
+ }
631
+
632
+ console.log('[RbtApi] Resolved organization ID:', orgId);
633
+
568
634
  if (this.localStorageAdaptor) {
569
635
  const cachedOrgId = await this.localStorageAdaptor.getItem('currentOrgId');
570
636
  if (cachedOrgId && cachedOrgId !== orgId) {
@@ -572,20 +638,16 @@ export default class RbtApi {
572
638
  }
573
639
  }
574
640
 
575
- // Load the organization object
576
- //console.log('[RbtApi] Loading organization:', orgId);
577
- const org = await this.get('<@iac.organization>', orgId);
578
-
641
+ const org = await this.load('<@iac.organization>', orgId);
642
+
579
643
  if (org) {
580
- this.currentOrganization = org; // Already an RbtObject
581
- //console.log('[RbtApi] Current organization loaded:', org.get('name'));
582
-
583
- // Cache in storage if available
644
+ this.currentOrganization = org;
584
645
  if (this.localStorageAdaptor) {
585
646
  await this.localStorageAdaptor.setItem('currentOrgId', orgId);
586
647
  }
587
648
  } else {
588
649
  console.warn('[RbtApi] Organization not found or not accessible:', orgId);
650
+ this.currentOrganization = null;
589
651
  }
590
652
 
591
653
  return this.currentOrganization;
@@ -621,7 +683,7 @@ export default class RbtApi {
621
683
  //console.log('[RbtApi] Switching to organization:', orgId);
622
684
 
623
685
  // Load the new organization
624
- const org = await this.get('<@iac.organization>', orgId);
686
+ const org = await this.load('<@iac.organization>', orgId);
625
687
  if (!org) {
626
688
  throw new Error(`Organization ${orgId} not found or not accessible`);
627
689
  }
@@ -631,7 +693,7 @@ export default class RbtApi {
631
693
  // Save preference to user
632
694
  try {
633
695
  const userObj = await this.loadUser(this.currentUser.id);
634
- userObj.set('mod.currentOrgId', orgId);
696
+ persistUserCurrentOrgPreference(userObj, orgId);
635
697
  await userObj.save();
636
698
 
637
699
  // Update cache
@@ -652,13 +714,17 @@ export default class RbtApi {
652
714
  }
653
715
 
654
716
  /**
655
- * Get current organization (null-safe)
656
- * Returns cached organization without triggering a load
657
- *
658
- * @returns {RbtObject|null} Current organization or null
717
+ * Get cached current organization (sync, null-safe).
718
+ * Returns **null** unless `mod.iac.currentOrgId` (or legacy `mod.currentOrgId`) matches `currentOrganization`.
659
719
  */
660
720
  getCurrentOrganization() {
661
- return this.currentOrganization;
721
+ const org = this.currentOrganization;
722
+ if (!org) return null;
723
+ const user = this.currentUser;
724
+ const selected = readUserCurrentOrgPreference(user);
725
+ if (!selected) return null;
726
+ if (String(org.id) !== selected) return null;
727
+ return org;
662
728
  }
663
729
 
664
730
  /**
@@ -683,8 +749,8 @@ export default class RbtApi {
683
749
 
684
750
  //console.log('[RbtApi] Selecting organization:', orgId);
685
751
 
686
- // Load the organization to verify access
687
- const org = await this.get('<@iac.organization>', orgId);
752
+ // Load the organization via object load (not HTTP `get` — passing orgId as params breaks axios).
753
+ const org = await this.load('<@iac.organization>', orgId);
688
754
  if (!org) {
689
755
  throw new Error(`Organization ${orgId} not found or not accessible`);
690
756
  }
@@ -709,11 +775,9 @@ export default class RbtApi {
709
775
  //console.log('[RbtApi] Organization already in user organizations array');
710
776
  }
711
777
 
712
- // Set as current organization preference - handle both nested path formats
713
- const modData = user.get('mod') || {};
714
- modData.currentOrgId = orgId;
715
- user.set('mod', modData);
716
-
778
+ // Set current organization preference under mod.iac (module-namespaced settings)
779
+ persistUserCurrentOrgPreference(user, orgId);
780
+
717
781
  // Save user record
718
782
  //console.log('[RbtApi] Saving user with organization:', orgId);
719
783
  await user.save();
@@ -756,7 +820,7 @@ export default class RbtApi {
756
820
 
757
821
  try {
758
822
  const p = (async () => {
759
- const response = await this.axios.post('/user_service/loadUser', [params]);
823
+ const response = await this.axios.post('/api/iac/loadUser', [params]);
760
824
  let userData = response?.data?.user;
761
825
  let User = new RbtUser({ id: userData.id }, this.axios);
762
826
  User.setData(userData);
@@ -788,7 +852,7 @@ export default class RbtApi {
788
852
  try {
789
853
  //console.log('RBTTOK Req', authtoken);
790
854
  // Create a new promise for the token refresh and store it in the cache
791
- const promise = this.axios.post('/user_service/refreshAuthToken', [authtoken]);
855
+ const promise = this.axios.post('/api/iac/refreshAuthToken', [authtoken]);
792
856
  this.requestCache[authtoken] = promise;
793
857
 
794
858
  // Await the promise to get the response
@@ -818,7 +882,7 @@ export default class RbtApi {
818
882
  //
819
883
  // try {
820
884
  //
821
- // const response = await this.axios.post('/user_service/refreshAuthToken', [authtoken]);
885
+ // const response = await this.axios.post('/api/iac/refreshAuthToken', [authtoken]);
822
886
  // return response.data;
823
887
  //
824
888
  // } catch (e) {
@@ -837,7 +901,7 @@ export default class RbtApi {
837
901
  */
838
902
  async registerUser(dataHash={}) {
839
903
  try {
840
- const response = await this.axios.post('/user_service/registerUser', [dataHash]);
904
+ const response = await this.axios.post('/api/iac/registerUser', [dataHash]);
841
905
 
842
906
  const record = response.data;
843
907
  return new RbtUser(record, this.axios);
@@ -956,6 +1020,7 @@ export default class RbtApi {
956
1020
  * - excludeAttrs: An array of attribute paths to exclude from the response (e.g., ['details.log', 'internalData']).
957
1021
  * Excludes the specified paths and all their subpaths.
958
1022
  * - resolveReferences: An array of attribute names whose references should be resolved in the returned objects.
1023
+ * - resolveAttrs: Attribute paths whose mod / reference values should be resolved (e.g., ['configs.mod.guide.completion']).
959
1024
  * - timeout: A numerical value in milliseconds to set a maximum time limit for the query execution.
960
1025
  * - cache: Boolean. When true, enables a 10-second response cache for identical queries. Default: false (no caching).
961
1026
  *
@@ -967,6 +1032,7 @@ export default class RbtApi {
967
1032
  * requestAttrs: ['id', 'configs.title'],
968
1033
  * excludeAttrs: ['details.log'],
969
1034
  * resolveReferences: ['translatableContent'],
1035
+ * resolveAttrs: ['configs.mod.guide.completion'],
970
1036
  * cache: true // opt-in to 10s response caching
971
1037
  * });
972
1038
  *
@@ -980,7 +1046,7 @@ export default class RbtApi {
980
1046
  //console.log('RBTAPI.query INIT', type, params);
981
1047
 
982
1048
  // Validate parameters - reject invalid parameter names
983
- const validParams = ['type', 'where', 'orderBy', 'limit', 'resolveReferences', 'requestAttrs', 'excludeAttrs', 'timeout', 'enableRealtime', 'cache'];
1049
+ const validParams = ['type', 'where', 'orderBy', 'limit', 'resolveReferences', 'resolveAttrs', 'requestAttrs', 'excludeAttrs', 'timeout', 'enableRealtime', 'cache'];
984
1050
  const invalidParams = Object.keys(params).filter(key => !validParams.includes(key));
985
1051
  if (invalidParams.length > 0) {
986
1052
  throw new Error(`Invalid query parameter(s): ${invalidParams.join(', ')}. Valid parameters are: ${validParams.join(', ')}`);
package/src/rbt_object.js CHANGED
@@ -259,12 +259,54 @@ export default class RbtObject{
259
259
  return clonedObject;
260
260
  }
261
261
 
262
+ /**
263
+ * Rebuild read_ids/write_ids record meta from iac grant arrays.
264
+ * Keeps denormalized columns in sync with dataJson.iac for delta saves.
265
+ */
266
+ _syncIacRecordMeta() {
267
+ const iac = _.get(this._data, 'iac');
268
+ if (!iac) return;
269
+
270
+ const readIds = new Set();
271
+ const writeIds = new Set();
272
+
273
+ if (this.type === '<@iac.user>') {
274
+ writeIds.add(this.id);
275
+ readIds.add(this.id);
276
+ }
277
+
278
+ if (iac.creator) {
279
+ writeIds.add(iac.creator);
280
+ readIds.add(iac.creator);
281
+ }
282
+
283
+ const collect = (grants, target) => {
284
+ if (!grants) return;
285
+ for (const key of ['users', 'userGroups', 'organizations', 'userSegments']) {
286
+ if (Array.isArray(grants[key])) {
287
+ grants[key].forEach((id) => target.add(id));
288
+ }
289
+ }
290
+ };
291
+ collect(iac.readGrants, readIds);
292
+ collect(iac.writeGrants, writeIds);
293
+
294
+ this._internalData.read_ids = Array.from(readIds).join(',');
295
+ this._internalData.write_ids = Array.from(writeIds).join(',');
296
+ }
297
+
262
298
  async save() {
263
299
  if (!this._internalData.type) {
264
300
  throw new Error('Cannot save object without type');
265
301
  }
266
302
 
267
303
  try {
304
+ const iacGrantChange = (this.rpcMeta.changes || []).some(
305
+ (path) => path.startsWith('iac.readGrants') || path.startsWith('iac.writeGrants')
306
+ );
307
+ if (iacGrantChange) {
308
+ this._syncIacRecordMeta();
309
+ }
268
310
 
269
311
  let record;
270
312
  if(this.rpcMeta.isNew){
@@ -399,6 +441,8 @@ export default class RbtObject{
399
441
  }
400
442
  }
401
443
 
444
+ this._syncIacRecordMeta();
445
+
402
446
  // Save if requested
403
447
  if (save) {
404
448
  return await this.save();
@@ -558,6 +602,8 @@ export default class RbtObject{
558
602
  this.set(groupsPath, existingGroups.filter(id => !groupIds.includes(id)));
559
603
  }
560
604
 
605
+ this._syncIacRecordMeta();
606
+
561
607
  // Save if requested
562
608
  if (save) {
563
609
  return await this.save();