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.
- package/CHANGELOG.md +89 -0
- package/lib/objects/Axis.d.ts +1 -0
- package/lib/objects/Axis.d.ts.map +1 -1
- package/lib/objects/Axis.js +3 -0
- package/lib/objects/Chore.d.ts +2 -2
- package/lib/objects/Chore.d.ts.map +1 -1
- package/lib/objects/Chore.js +7 -13
- package/lib/objects/Cube.d.ts.map +1 -1
- package/lib/objects/Cube.js +2 -1
- package/lib/objects/Hierarchy.js +10 -10
- package/lib/objects/MDXView.d.ts +2 -0
- package/lib/objects/MDXView.d.ts.map +1 -1
- package/lib/objects/MDXView.js +30 -9
- package/lib/objects/NativeView.d.ts +5 -5
- package/lib/objects/NativeView.d.ts.map +1 -1
- package/lib/objects/NativeView.js +17 -34
- package/lib/objects/Process.d.ts +8 -3
- package/lib/objects/Process.d.ts.map +1 -1
- package/lib/objects/Process.js +143 -33
- package/lib/objects/Subset.d.ts.map +1 -1
- package/lib/objects/Subset.js +10 -3
- package/lib/objects/User.d.ts +5 -5
- package/lib/objects/User.d.ts.map +1 -1
- package/lib/objects/User.js +14 -23
- package/lib/services/ApplicationService.d.ts.map +1 -1
- package/lib/services/AsyncOperationService.d.ts +8 -1
- package/lib/services/AsyncOperationService.d.ts.map +1 -1
- package/lib/services/AsyncOperationService.js +69 -26
- package/lib/services/FileService.d.ts.map +1 -1
- package/lib/services/ProcessService.d.ts +18 -13
- package/lib/services/ProcessService.d.ts.map +1 -1
- package/lib/services/ProcessService.js +28 -17
- package/lib/services/RestService.d.ts +213 -25
- package/lib/services/RestService.d.ts.map +1 -1
- package/lib/services/RestService.js +840 -271
- package/lib/services/TM1Service.d.ts +42 -1
- package/lib/services/TM1Service.d.ts.map +1 -1
- package/lib/services/TM1Service.js +94 -4
- package/lib/tests/asyncOperationService.test.js +51 -45
- package/lib/tests/debuggerService.test.js +3 -1
- package/lib/tests/objectModelParity.test.js +362 -11
- package/lib/tests/objects.improved.test.js +28 -5
- package/lib/tests/processService.comprehensive.test.js +2 -2
- package/lib/tests/processService.test.js +20 -6
- package/lib/tests/restService.test.d.ts +0 -4
- package/lib/tests/restService.test.d.ts.map +1 -1
- package/lib/tests/restService.test.js +1558 -143
- package/lib/tests/tm1Service.test.js +80 -8
- package/lib/tests/user.issue61.test.d.ts +2 -0
- package/lib/tests/user.issue61.test.d.ts.map +1 -0
- package/lib/tests/user.issue61.test.js +180 -0
- package/lib/utils/Utils.d.ts +6 -1
- package/lib/utils/Utils.d.ts.map +1 -1
- package/lib/utils/Utils.js +56 -7
- package/package.json +1 -1
- package/src/objects/Axis.ts +4 -0
- package/src/objects/Chore.ts +7 -14
- package/src/objects/Cube.ts +2 -1
- package/src/objects/Hierarchy.ts +11 -11
- package/src/objects/MDXView.ts +29 -9
- package/src/objects/NativeView.ts +26 -42
- package/src/objects/Process.ts +182 -66
- package/src/objects/Subset.ts +17 -3
- package/src/objects/User.ts +17 -23
- package/src/services/ApplicationService.ts +4 -4
- package/src/services/AsyncOperationService.ts +76 -29
- package/src/services/FileService.ts +3 -3
- package/src/services/ProcessService.ts +67 -37
- package/src/services/RestService.ts +1020 -278
- package/src/services/TM1Service.ts +124 -6
- package/src/tests/asyncOperationService.test.ts +52 -48
- package/src/tests/debuggerService.test.ts +3 -1
- package/src/tests/objectModelParity.test.ts +456 -11
- package/src/tests/objects.improved.test.ts +41 -9
- package/src/tests/processService.comprehensive.test.ts +3 -3
- package/src/tests/processService.test.ts +21 -9
- package/src/tests/restService.test.ts +1844 -139
- package/src/tests/tm1Service.test.ts +95 -11
- package/src/tests/user.issue61.test.ts +206 -0
- 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
|
-
|
|
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(
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|
package/src/utils/Utils.ts
CHANGED
|
@@ -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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
90
|
+
return this._normalizedMap.has(lowerAndDropSpaces(value));
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
delete(value: string): boolean {
|
|
81
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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,
|