tm1npm 2.0.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/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/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/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/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/user.issue61.test.ts +206 -0
- package/src/utils/Utils.ts +60 -7
|
@@ -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,
|