tm1npm 1.6.0 → 2.1.0

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 (80) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/lib/objects/Axis.d.ts +1 -0
  3. package/lib/objects/Axis.d.ts.map +1 -1
  4. package/lib/objects/Axis.js +3 -0
  5. package/lib/objects/Chore.d.ts +2 -2
  6. package/lib/objects/Chore.d.ts.map +1 -1
  7. package/lib/objects/Chore.js +7 -13
  8. package/lib/objects/Cube.d.ts.map +1 -1
  9. package/lib/objects/Cube.js +2 -1
  10. package/lib/objects/Hierarchy.js +10 -10
  11. package/lib/objects/MDXView.d.ts +2 -0
  12. package/lib/objects/MDXView.d.ts.map +1 -1
  13. package/lib/objects/MDXView.js +30 -9
  14. package/lib/objects/NativeView.d.ts +5 -5
  15. package/lib/objects/NativeView.d.ts.map +1 -1
  16. package/lib/objects/NativeView.js +17 -34
  17. package/lib/objects/Process.d.ts +8 -3
  18. package/lib/objects/Process.d.ts.map +1 -1
  19. package/lib/objects/Process.js +143 -33
  20. package/lib/objects/Subset.d.ts.map +1 -1
  21. package/lib/objects/Subset.js +10 -3
  22. package/lib/objects/User.d.ts +5 -5
  23. package/lib/objects/User.d.ts.map +1 -1
  24. package/lib/objects/User.js +14 -23
  25. package/lib/services/ApplicationService.d.ts.map +1 -1
  26. package/lib/services/AsyncOperationService.d.ts +8 -1
  27. package/lib/services/AsyncOperationService.d.ts.map +1 -1
  28. package/lib/services/AsyncOperationService.js +69 -26
  29. package/lib/services/FileService.d.ts.map +1 -1
  30. package/lib/services/ProcessService.d.ts +18 -13
  31. package/lib/services/ProcessService.d.ts.map +1 -1
  32. package/lib/services/ProcessService.js +28 -17
  33. package/lib/services/RestService.d.ts +213 -25
  34. package/lib/services/RestService.d.ts.map +1 -1
  35. package/lib/services/RestService.js +840 -271
  36. package/lib/services/TM1Service.d.ts +42 -1
  37. package/lib/services/TM1Service.d.ts.map +1 -1
  38. package/lib/services/TM1Service.js +94 -4
  39. package/lib/tests/asyncOperationService.test.js +51 -45
  40. package/lib/tests/debuggerService.test.js +3 -1
  41. package/lib/tests/objectModelParity.test.js +362 -11
  42. package/lib/tests/objects.improved.test.js +28 -5
  43. package/lib/tests/processService.comprehensive.test.js +2 -2
  44. package/lib/tests/processService.test.js +20 -6
  45. package/lib/tests/restService.test.d.ts +0 -4
  46. package/lib/tests/restService.test.d.ts.map +1 -1
  47. package/lib/tests/restService.test.js +1558 -143
  48. package/lib/tests/tm1Service.test.js +80 -8
  49. package/lib/tests/user.issue61.test.d.ts +2 -0
  50. package/lib/tests/user.issue61.test.d.ts.map +1 -0
  51. package/lib/tests/user.issue61.test.js +180 -0
  52. package/lib/utils/Utils.d.ts +6 -1
  53. package/lib/utils/Utils.d.ts.map +1 -1
  54. package/lib/utils/Utils.js +56 -7
  55. package/package.json +1 -1
  56. package/src/objects/Axis.ts +4 -0
  57. package/src/objects/Chore.ts +7 -14
  58. package/src/objects/Cube.ts +2 -1
  59. package/src/objects/Hierarchy.ts +11 -11
  60. package/src/objects/MDXView.ts +29 -9
  61. package/src/objects/NativeView.ts +26 -42
  62. package/src/objects/Process.ts +182 -66
  63. package/src/objects/Subset.ts +17 -3
  64. package/src/objects/User.ts +17 -23
  65. package/src/services/ApplicationService.ts +4 -4
  66. package/src/services/AsyncOperationService.ts +76 -29
  67. package/src/services/FileService.ts +3 -3
  68. package/src/services/ProcessService.ts +67 -37
  69. package/src/services/RestService.ts +1020 -278
  70. package/src/services/TM1Service.ts +124 -6
  71. package/src/tests/asyncOperationService.test.ts +52 -48
  72. package/src/tests/debuggerService.test.ts +3 -1
  73. package/src/tests/objectModelParity.test.ts +456 -11
  74. package/src/tests/objects.improved.test.ts +41 -9
  75. package/src/tests/processService.comprehensive.test.ts +3 -3
  76. package/src/tests/processService.test.ts +21 -9
  77. package/src/tests/restService.test.ts +1844 -139
  78. package/src/tests/tm1Service.test.ts +95 -11
  79. package/src/tests/user.issue61.test.ts +206 -0
  80. package/src/utils/Utils.ts +60 -7
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { TM1Service } from '../services/TM1Service';
8
8
  import { RestService, RestServiceConfig } from '../services/RestService';
9
+ import { User, UserType } from '../objects/User';
9
10
 
10
11
  // Mock all service dependencies
11
12
  jest.mock('../services/RestService');
@@ -22,6 +23,19 @@ jest.mock('../services/FileService');
22
23
  jest.mock('../services/SessionService');
23
24
  jest.mock('../services/ServerService');
24
25
  jest.mock('../services/MonitoringService');
26
+ jest.mock('../services/AnnotationService');
27
+ jest.mock('../services/ChoreService');
28
+ jest.mock('../services/GitService');
29
+ jest.mock('../services/ApplicationService');
30
+ jest.mock('../services/SandboxService');
31
+ jest.mock('../services/JobService');
32
+ jest.mock('../services/UserService');
33
+ jest.mock('../services/ThreadService');
34
+ jest.mock('../services/TransactionLogService');
35
+ jest.mock('../services/MessageLogService');
36
+ jest.mock('../services/ConfigurationService');
37
+ jest.mock('../services/AuditLogService');
38
+ jest.mock('../services/LoggerService');
25
39
 
26
40
  describe('TM1Service', () => {
27
41
  let tm1Service: TM1Service;
@@ -61,6 +75,8 @@ describe('TM1Service', () => {
61
75
  setSandbox: jest.fn(),
62
76
  getSandbox: jest.fn().mockReturnValue('test-sandbox'),
63
77
  isLoggedIn: jest.fn().mockReturnValue(true),
78
+ getVersion: jest.fn().mockResolvedValue('12.0.0'),
79
+ version: undefined,
64
80
  } as any;
65
81
 
66
82
  // Mock RestService constructor
@@ -83,6 +99,7 @@ describe('TM1Service', () => {
83
99
  expect(tm1Service.security).toBeDefined();
84
100
  expect(tm1Service.files).toBeDefined();
85
101
  expect(tm1Service.sessions).toBeDefined();
102
+ expect(tm1Service.applications).toBeDefined();
86
103
  });
87
104
 
88
105
  test('should create RestService with provided config', () => {
@@ -145,16 +162,18 @@ describe('TM1Service', () => {
145
162
  });
146
163
 
147
164
  describe('User and Authentication', () => {
148
- test('should get current user with whoami', async () => {
149
- // Mock security service getCurrentUser method
165
+ test('should get current user as User object with whoami', async () => {
166
+ const expectedUser = new User('test-user', ['ADMIN'], 'Test User', undefined, UserType.Admin, true);
150
167
  const mockSecurityService = {
151
- getCurrentUser: jest.fn().mockResolvedValue({ name: 'test-user' })
168
+ getCurrentUser: jest.fn().mockResolvedValue(expectedUser)
152
169
  };
153
170
  (tm1Service.security as any) = mockSecurityService;
154
171
 
155
172
  const result = await tm1Service.whoami();
156
-
157
- expect(result).toBe('test-user');
173
+
174
+ expect(result).toBeInstanceOf(User);
175
+ expect(result.name).toBe('test-user');
176
+ expect(result).toBe(expectedUser);
158
177
  expect(mockSecurityService.getCurrentUser).toHaveBeenCalledTimes(1);
159
178
  });
160
179
 
@@ -175,10 +194,11 @@ describe('TM1Service', () => {
175
194
 
176
195
  test('should re-authenticate successfully', async () => {
177
196
  await tm1Service.reAuthenticate();
178
-
197
+
179
198
  expect(mockRestService.disconnect).toHaveBeenCalledTimes(1);
180
199
  expect(mockRestService.connect).toHaveBeenCalledTimes(1);
181
200
  });
201
+
182
202
  });
183
203
 
184
204
  describe('Metadata and Version', () => {
@@ -191,13 +211,13 @@ describe('TM1Service', () => {
191
211
  expect(mockRestService.get).toHaveBeenCalledWith('/$metadata');
192
212
  });
193
213
 
194
- test('should get TM1 version', async () => {
195
- mockRestService.get.mockResolvedValueOnce(mockResponse({ value: '12.0.0' }));
214
+ test('should get TM1 version via cached RestService.getVersion', async () => {
215
+ mockRestService.getVersion.mockResolvedValueOnce('12.0.0');
196
216
 
197
217
  const result = await tm1Service.getVersion();
198
-
218
+
199
219
  expect(result).toBe('12.0.0');
200
- expect(mockRestService.get).toHaveBeenCalledWith('/Configuration/ProductVersion');
220
+ expect(mockRestService.getVersion).toHaveBeenCalledTimes(1);
201
221
  });
202
222
 
203
223
  test('should handle metadata retrieval errors', async () => {
@@ -209,7 +229,7 @@ describe('TM1Service', () => {
209
229
 
210
230
  test('should handle version retrieval errors', async () => {
211
231
  const versionError = new Error('Version not available');
212
- mockRestService.get.mockRejectedValueOnce(versionError);
232
+ mockRestService.getVersion.mockRejectedValueOnce(versionError);
213
233
 
214
234
  await expect(tm1Service.getVersion()).rejects.toThrow('Version not available');
215
235
  });
@@ -358,4 +378,68 @@ describe('TM1Service', () => {
358
378
  expect(monitoring1).toBe(monitoring2);
359
379
  });
360
380
  });
381
+
382
+ describe('Lazy Services (Issue #82)', () => {
383
+ const lazyServiceNames: Array<keyof TM1Service> = [
384
+ 'annotations',
385
+ 'chores',
386
+ 'git',
387
+ 'sandboxes',
388
+ 'jobs',
389
+ 'users',
390
+ 'threads',
391
+ 'transactionLogs',
392
+ 'messageLogs',
393
+ 'configuration',
394
+ 'auditLogs',
395
+ 'loggers',
396
+ ];
397
+
398
+ test.each(lazyServiceNames)('should lazy-initialize %s service', (serviceName) => {
399
+ const instance = tm1Service[serviceName];
400
+ expect(instance).toBeDefined();
401
+ });
402
+
403
+ test.each(lazyServiceNames)('should cache %s service instance across accesses', (serviceName) => {
404
+ const first = tm1Service[serviceName];
405
+ const second = tm1Service[serviceName];
406
+ expect(second).toBe(first);
407
+ });
408
+ });
409
+
410
+ describe('Version Getter (Issue #82)', () => {
411
+ test('should expose cached version via sync getter', () => {
412
+ Object.defineProperty(mockRestService, 'version', {
413
+ get: () => '11.8.0',
414
+ configurable: true,
415
+ });
416
+
417
+ expect(tm1Service.version).toBe('11.8.0');
418
+ });
419
+
420
+ test('should return undefined when version has not been fetched yet', () => {
421
+ Object.defineProperty(mockRestService, 'version', {
422
+ get: () => undefined,
423
+ configurable: true,
424
+ });
425
+
426
+ expect(tm1Service.version).toBeUndefined();
427
+ });
428
+ });
429
+
430
+ describe('reConnect (Issue #82)', () => {
431
+ test('should call connect without disconnecting (tm1py parity)', async () => {
432
+ await tm1Service.reConnect();
433
+
434
+ expect(mockRestService.connect).toHaveBeenCalledTimes(1);
435
+ expect(mockRestService.disconnect).not.toHaveBeenCalled();
436
+ });
437
+
438
+ test('should propagate errors from connect', async () => {
439
+ mockRestService.connect.mockRejectedValueOnce(new Error('Connect failed'));
440
+
441
+ await expect(tm1Service.reConnect()).rejects.toThrow('Connect failed');
442
+ expect(mockRestService.connect).toHaveBeenCalledTimes(1);
443
+ });
444
+ });
361
445
  });
@@ -0,0 +1,206 @@
1
+ import { User, UserType } from '../objects/User';
2
+ import { CaseAndSpaceInsensitiveSet } from '../utils/Utils';
3
+
4
+ describe('UserType — string enum (issue #61)', () => {
5
+ test('should produce correct string names from toString()', () => {
6
+ expect(UserType.User.toString()).toBe('User');
7
+ expect(UserType.SecurityAdmin.toString()).toBe('SecurityAdmin');
8
+ expect(UserType.DataAdmin.toString()).toBe('DataAdmin');
9
+ expect(UserType.Admin.toString()).toBe('Admin');
10
+ expect(UserType.OperationsAdmin.toString()).toBe('OperationsAdmin');
11
+ });
12
+
13
+ test('should serialize Type field as string name in body', () => {
14
+ const user = new User('Alice', [], undefined, undefined, UserType.Admin);
15
+ const body = JSON.parse(user.body);
16
+ expect(body.Type).toBe('Admin');
17
+ });
18
+
19
+ test('should serialize User type as "User" string in body', () => {
20
+ const user = new User('Alice', []);
21
+ const body = JSON.parse(user.body);
22
+ expect(body.Type).toBe('User');
23
+ });
24
+
25
+ test('should parse string value in userType setter', () => {
26
+ const user = new User('Alice', []);
27
+ user.userType = 'Admin';
28
+ expect(user.userType).toBe(UserType.Admin);
29
+ });
30
+
31
+ test('should parse case-insensitive string in userType setter', () => {
32
+ const user = new User('Alice', []);
33
+ user.userType = 'admin';
34
+ expect(user.userType).toBe(UserType.Admin);
35
+ });
36
+
37
+ test('should throw on invalid userType string', () => {
38
+ const user = new User('Alice', []);
39
+ expect(() => { user.userType = 'InvalidType'; }).toThrow();
40
+ });
41
+ });
42
+
43
+ describe('User — group auto-detection (issue #61)', () => {
44
+ test('should detect Admin type from groups', () => {
45
+ const user = new User('Alice', ['Admin']);
46
+ expect(user.userType).toBe(UserType.Admin);
47
+ });
48
+
49
+ test('should detect SecurityAdmin type from groups', () => {
50
+ const user = new User('Alice', ['SecurityAdmin']);
51
+ expect(user.userType).toBe(UserType.SecurityAdmin);
52
+ });
53
+
54
+ test('should detect DataAdmin type from groups', () => {
55
+ const user = new User('Alice', ['DataAdmin']);
56
+ expect(user.userType).toBe(UserType.DataAdmin);
57
+ });
58
+
59
+ test('should detect OperationsAdmin type from groups', () => {
60
+ const user = new User('Alice', ['OperationsAdmin']);
61
+ expect(user.userType).toBe(UserType.OperationsAdmin);
62
+ });
63
+
64
+ test('should default to User type when no special groups present', () => {
65
+ const user = new User('Alice', ['Everyone', 'SomeTeam']);
66
+ expect(user.userType).toBe(UserType.User);
67
+ });
68
+
69
+ test('should add correct string group when userType set to Admin', () => {
70
+ const user = new User('Alice', []);
71
+ user.userType = UserType.Admin;
72
+ expect(user.groups).toContain('Admin');
73
+ });
74
+
75
+ test('should not add a group when userType set to User', () => {
76
+ const user = new User('Alice', []);
77
+ user.userType = UserType.User;
78
+ expect(user.groups).toHaveLength(0);
79
+ });
80
+ });
81
+
82
+ describe('User — isXxxAdmin getters (issue #61)', () => {
83
+ test('should return true for isAdmin when Admin group present', () => {
84
+ const user = new User('Alice', ['Admin']);
85
+ expect(user.isAdmin).toBe(true);
86
+ });
87
+
88
+ test('should return false for isAdmin when Admin group absent', () => {
89
+ const user = new User('Alice', ['Everyone']);
90
+ expect(user.isAdmin).toBe(false);
91
+ });
92
+
93
+ test('should return true for isDataAdmin when Admin group present', () => {
94
+ const user = new User('Alice', ['Admin']);
95
+ expect(user.isDataAdmin).toBe(true);
96
+ });
97
+
98
+ test('should return true for isDataAdmin when DataAdmin group present', () => {
99
+ const user = new User('Alice', ['DataAdmin']);
100
+ expect(user.isDataAdmin).toBe(true);
101
+ });
102
+
103
+ test('should return true for isSecurityAdmin when SecurityAdmin group present', () => {
104
+ const user = new User('Alice', ['SecurityAdmin']);
105
+ expect(user.isSecurityAdmin).toBe(true);
106
+ });
107
+
108
+ test('should return true for isOpsAdmin when OperationsAdmin group present', () => {
109
+ const user = new User('Alice', ['OperationsAdmin']);
110
+ expect(user.isOpsAdmin).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe('User.fromDict — Type field parsing (issue #61)', () => {
115
+ test('should parse Type string field from API response dict', () => {
116
+ const dict = {
117
+ Name: 'Alice',
118
+ FriendlyName: 'Alice',
119
+ Groups: [{ Name: 'Admin' }],
120
+ Type: 'Admin',
121
+ Enabled: true,
122
+ };
123
+ const user = User.fromDict(dict);
124
+ expect(user.userType).toBe(UserType.Admin);
125
+ });
126
+
127
+ test('should parse SecurityAdmin Type field from API response dict', () => {
128
+ const dict = {
129
+ Name: 'Bob',
130
+ FriendlyName: 'Bob',
131
+ Groups: [{ Name: 'SecurityAdmin' }],
132
+ Type: 'SecurityAdmin',
133
+ Enabled: true,
134
+ };
135
+ const user = User.fromDict(dict);
136
+ expect(user.userType).toBe(UserType.SecurityAdmin);
137
+ });
138
+ });
139
+
140
+ describe('CaseAndSpaceInsensitiveSet — casing preservation (issue #61)', () => {
141
+ test('should preserve original casing of first insertion', () => {
142
+ const set = new CaseAndSpaceInsensitiveSet();
143
+ set.add('Admin');
144
+ set.add('ADMIN');
145
+ set.add('admin');
146
+ expect(set.size).toBe(1);
147
+ expect(Array.from(set)).toEqual(['Admin']);
148
+ });
149
+
150
+ test('should find by any casing variant', () => {
151
+ const set = new CaseAndSpaceInsensitiveSet();
152
+ set.add('HelloWorld');
153
+ expect(set.has('helloworld')).toBe(true);
154
+ expect(set.has('HELLOWORLD')).toBe(true);
155
+ expect(set.has('Hello World')).toBe(true);
156
+ expect(set.has('hello world')).toBe(true);
157
+ });
158
+
159
+ test('should return first-inserted casing during iteration', () => {
160
+ const set = new CaseAndSpaceInsensitiveSet();
161
+ set.add('MyGroup');
162
+ set.add('mygroup');
163
+ set.add('MYGROUP');
164
+ expect(Array.from(set)).toEqual(['MyGroup']);
165
+ });
166
+
167
+ test('should delete by any casing variant', () => {
168
+ const set = new CaseAndSpaceInsensitiveSet();
169
+ set.add('Admin');
170
+ expect(set.delete('ADMIN')).toBe(true);
171
+ expect(set.size).toBe(0);
172
+ expect(set.has('Admin')).toBe(false);
173
+ });
174
+
175
+ test('should return false when deleting non-existent entry', () => {
176
+ const set = new CaseAndSpaceInsensitiveSet();
177
+ expect(set.delete('NonExistent')).toBe(false);
178
+ });
179
+
180
+ test('should maintain correct size after operations', () => {
181
+ const set = new CaseAndSpaceInsensitiveSet();
182
+ set.add('A');
183
+ set.add('B');
184
+ set.add('a'); // duplicate — ignored
185
+ expect(set.size).toBe(2);
186
+ set.delete('A');
187
+ expect(set.size).toBe(1);
188
+ });
189
+
190
+ test('should clear all entries', () => {
191
+ const set = new CaseAndSpaceInsensitiveSet();
192
+ set.add('A');
193
+ set.add('B');
194
+ set.clear();
195
+ expect(set.size).toBe(0);
196
+ expect(set.has('A')).toBe(false);
197
+ });
198
+
199
+ test('should handle values with spaces', () => {
200
+ const set = new CaseAndSpaceInsensitiveSet();
201
+ set.add('Data Admin');
202
+ expect(set.has('dataadmin')).toBe(true);
203
+ expect(set.has('DataAdmin')).toBe(true);
204
+ expect(Array.from(set)).toEqual(['Data Admin']);
205
+ });
206
+ });
@@ -65,20 +65,42 @@ export class CaseAndSpaceInsensitiveMap<T> extends Map<string, T> {
65
65
  }
66
66
 
67
67
  export class CaseAndSpaceInsensitiveSet extends Set<string> {
68
- private normalizeValue(value: string): string {
69
- return value.toLowerCase().replace(/\s+/g, '');
68
+ private _normalizedMap: Map<string, string>;
69
+
70
+ constructor(values?: Iterable<string>) {
71
+ super();
72
+ this._normalizedMap = new Map<string, string>();
73
+ if (values) {
74
+ for (const v of values) {
75
+ this.add(v);
76
+ }
77
+ }
70
78
  }
71
79
 
72
80
  add(value: string): this {
73
- return super.add(this.normalizeValue(value));
81
+ const key = lowerAndDropSpaces(value);
82
+ if (!this._normalizedMap.has(key)) {
83
+ this._normalizedMap.set(key, value);
84
+ super.add(value);
85
+ }
86
+ return this;
74
87
  }
75
88
 
76
89
  has(value: string): boolean {
77
- return super.has(this.normalizeValue(value));
90
+ return this._normalizedMap.has(lowerAndDropSpaces(value));
78
91
  }
79
92
 
80
93
  delete(value: string): boolean {
81
- return super.delete(this.normalizeValue(value));
94
+ const key = lowerAndDropSpaces(value);
95
+ const original = this._normalizedMap.get(key);
96
+ if (original === undefined) return false;
97
+ this._normalizedMap.delete(key);
98
+ return super.delete(original);
99
+ }
100
+
101
+ clear(): void {
102
+ this._normalizedMap.clear();
103
+ super.clear();
82
104
  }
83
105
  }
84
106
 
@@ -98,6 +120,15 @@ export function escapeODataValue(str: string): string {
98
120
  return str.replace(/'/g, "''");
99
121
  }
100
122
 
123
+ export function buildUrlFriendlyObjectName(objectName: string): string {
124
+ return objectName
125
+ .replace(/'/g, "''")
126
+ .replace(/%/g, "%25")
127
+ .replace(/#/g, "%23")
128
+ .replace(/\?/g, "%3F")
129
+ .replace(/&/g, "%26");
130
+ }
131
+
101
132
  export function formatUrl(template: string, ...args: string[]): string {
102
133
  let url = template;
103
134
  for (const arg of args) {
@@ -336,8 +367,29 @@ export function verifyVersion(actualVersion: string, requiredVersion: string): b
336
367
  return true; // Equal versions
337
368
  }
338
369
 
339
- export function readObjectNameFromUrl(url: string): string {
340
- // Extract object name from URL like "/Dimensions('DimName')" -> "DimName"
370
+ // Overloads preserve the narrow `string` return for the existing no-pattern form
371
+ // (published npm API widening to `string | null` would break downstream consumers).
372
+ export function readObjectNameFromUrl(url: string): string;
373
+ export function readObjectNameFromUrl(url: string, pattern: string | RegExp): string | null;
374
+ export function readObjectNameFromUrl(url: string, pattern?: string | RegExp): string | null {
375
+ // tm1py parity: when `pattern` is provided, behave like
376
+ // `re.match(pattern, url)` + `unquote(match.group(1))` — anchored at start,
377
+ // URL-decodes the first capture group, returns null on no match.
378
+ if (pattern !== undefined) {
379
+ const re = typeof pattern === 'string'
380
+ ? new RegExp(pattern.startsWith('^') ? pattern : '^' + pattern)
381
+ : pattern;
382
+ const m = url.match(re);
383
+ if (!m || m[1] === undefined) return null;
384
+ try {
385
+ return decodeURIComponent(m[1]);
386
+ } catch {
387
+ // tm1py parity: urllib.parse.unquote is permissive on malformed %XX
388
+ // (returns the original string). decodeURIComponent throws — fall back.
389
+ return m[1];
390
+ }
391
+ }
392
+ // Backward-compat: extract first ('name') segment, return '' on no match
341
393
  const match = url.match(/\('([^']+)'\)/);
342
394
  return match ? match[1] : '';
343
395
  }
@@ -395,6 +447,7 @@ export const Utils = {
395
447
  CaseAndSpaceInsensitiveSet,
396
448
  caseAndSpaceInsensitiveEquals,
397
449
  lowerAndDropSpaces,
450
+ buildUrlFriendlyObjectName,
398
451
  formatUrl,
399
452
  extractCellsetCells,
400
453
  buildMdxFromAxes,