@sisense/sdk-common 2.5.0 → 2.7.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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Normalizes a URL string by ensuring it has a trailing slash and optionally includes search parameters.
3
+ * @param url - The URL string to normalize
4
+ * @param includeSearchString - Whether to include search parameters in the result
5
+ * @returns The normalized URL string
6
+ * @throws Error if the URL is invalid, empty, or not a string
7
+ */
8
+ export function normalizeUrl(url, includeSearchString) {
9
+ if (typeof url !== 'string') {
10
+ throw new Error('URL must be a string');
11
+ }
12
+ if (!url.trim()) {
13
+ throw new Error('URL cannot be empty');
14
+ }
15
+ try {
16
+ const urlObject = new URL(url);
17
+ return (urlObject.origin +
18
+ (urlObject.pathname.endsWith('/') ? urlObject.pathname : urlObject.pathname + '/') +
19
+ (includeSearchString && urlObject.search ? urlObject.search : ''));
20
+ }
21
+ catch (_a) {
22
+ throw new Error(`Connection string ${url} is not a valid URL`);
23
+ }
24
+ }
25
+ /**
26
+ * Merges two URLs, combining their paths and query parameters.
27
+ * If loginUrlStr is an absolute URL, it takes precedence over baseUrlStr.
28
+ * If loginUrlStr is relative, it's merged with baseUrlStr.
29
+ *
30
+ * @param baseUrlStr - The base URL to merge with
31
+ * @param loginUrlStr - The login URL (can be absolute or relative)
32
+ * @returns The merged URL string
33
+ * @throws Error if baseUrlStr is not a valid URL
34
+ */
35
+ export function mergeUrlsWithParams(baseUrlStr, loginUrlStr) {
36
+ if (!baseUrlStr) {
37
+ throw new Error('Base URL is required');
38
+ }
39
+ // Parse base URL first to validate it
40
+ let baseUrl;
41
+ try {
42
+ baseUrl = new URL(baseUrlStr);
43
+ }
44
+ catch (_a) {
45
+ throw new Error('Base URL is not valid');
46
+ }
47
+ // If no login URL provided or it's just a slash, return base URL
48
+ if (!loginUrlStr || loginUrlStr === '/') {
49
+ return baseUrl.toString();
50
+ }
51
+ try {
52
+ // Check if loginUrlStr is an absolute URL
53
+ const loginUrl = new URL(loginUrlStr);
54
+ return loginUrl.toString();
55
+ }
56
+ catch (_b) {
57
+ // loginUrlStr is relative, proceed with merging
58
+ }
59
+ // Create a URL by joining base origin with login path
60
+ const resultUrl = new URL(baseUrl.toString());
61
+ // Split login URL into path and query parts
62
+ const [loginPath = '', loginQuery = ''] = loginUrlStr.split(/\?(.+)/);
63
+ // Handle the path merging
64
+ const basePath = baseUrl.pathname.replace(/\/$/, ''); // Remove trailing slash
65
+ const cleanLoginPath = loginPath
66
+ .replace(/^\/+/, '') // Remove leading slashes
67
+ .replace(/\/+$/, '') // Remove trailing slashes
68
+ .replace(/\/+/g, '/'); // Replace multiple slashes with single slash
69
+ resultUrl.pathname = cleanLoginPath ? `${basePath}/${cleanLoginPath}` : basePath;
70
+ // Parse and merge query parameters from both URLs
71
+ if (loginQuery) {
72
+ const loginParams = new URLSearchParams(loginQuery);
73
+ for (const [key, value] of loginParams.entries()) {
74
+ resultUrl.searchParams.set(key, value);
75
+ }
76
+ }
77
+ // Normalize the final URL by removing trailing slash
78
+ return normalizeUrl(resultUrl.toString(), true);
79
+ }
@@ -0,0 +1,94 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mergeUrlsWithParams, normalizeUrl } from './url.js';
3
+ describe('normalizeUrl', () => {
4
+ it("should add a trailing slash if url doesn't have it", () => {
5
+ expect(normalizeUrl('https://example.com')).toBe('https://example.com/');
6
+ });
7
+ it('should not add an extra trailing slash if url does have it', () => {
8
+ expect(normalizeUrl('https://example.com/')).toBe('https://example.com/');
9
+ });
10
+ it('should include search string when specified', () => {
11
+ expect(normalizeUrl('https://example.com?param=value', true)).toBe('https://example.com/?param=value');
12
+ });
13
+ it('should throw error for invalid URL', () => {
14
+ expect(() => normalizeUrl('not-a-url')).toThrow();
15
+ });
16
+ it('should throw error for empty string', () => {
17
+ expect(() => normalizeUrl('')).toThrow('URL cannot be empty');
18
+ expect(() => normalizeUrl(' ')).toThrow('URL cannot be empty');
19
+ });
20
+ it('should throw error for non-string input', () => {
21
+ // @ts-expect-error Testing runtime type check
22
+ expect(() => normalizeUrl(null)).toThrow('URL must be a string');
23
+ // @ts-expect-error Testing runtime type check
24
+ expect(() => normalizeUrl(undefined)).toThrow('URL must be a string');
25
+ // @ts-expect-error Testing runtime type check
26
+ expect(() => normalizeUrl(123)).toThrow('URL must be a string');
27
+ });
28
+ });
29
+ describe('mergeUrlsWithParams', () => {
30
+ describe('input validation', () => {
31
+ it('should throw error when base URL is not provided', () => {
32
+ expect(() => mergeUrlsWithParams('')).toThrow('Base URL is required');
33
+ });
34
+ it('should throw error when base URL is invalid', () => {
35
+ expect(() => mergeUrlsWithParams('not-a-url')).toThrow('Base URL is not valid');
36
+ });
37
+ });
38
+ describe('base URL handling', () => {
39
+ it('should return base URL when login URL is not provided', () => {
40
+ expect(mergeUrlsWithParams('https://example.com')).toBe('https://example.com/');
41
+ });
42
+ it('should return base URL when login URL is just a slash', () => {
43
+ expect(mergeUrlsWithParams('https://example.com', '/')).toBe('https://example.com/');
44
+ });
45
+ it('should preserve base URL port if specified', () => {
46
+ expect(mergeUrlsWithParams('https://example.com:8080', 'login')).toBe('https://example.com:8080/login/');
47
+ });
48
+ });
49
+ describe('absolute URL handling', () => {
50
+ it('should use absolute login URL when provided', () => {
51
+ expect(mergeUrlsWithParams('https://example.com', 'https://login.example.com/')).toBe('https://login.example.com/');
52
+ });
53
+ it('should preserve query params in absolute login URL', () => {
54
+ expect(mergeUrlsWithParams('https://example.com', 'https://login.example.com?token=123')).toBe('https://login.example.com/?token=123');
55
+ });
56
+ });
57
+ describe('relative path handling', () => {
58
+ it('should merge relative login path with base URL', () => {
59
+ expect(mergeUrlsWithParams('https://example.com', 'login')).toBe('https://example.com/login/');
60
+ });
61
+ it('should handle relative login path with leading slash', () => {
62
+ expect(mergeUrlsWithParams('https://example.com', '/login')).toBe('https://example.com/login/');
63
+ });
64
+ it('should handle relative login path with trailing slash', () => {
65
+ expect(mergeUrlsWithParams('https://example.com', 'login/')).toBe('https://example.com/login/');
66
+ });
67
+ it('should handle multiple slashes in path', () => {
68
+ expect(mergeUrlsWithParams('https://example.com', '//login///path//')).toBe('https://example.com/login/path/');
69
+ });
70
+ });
71
+ describe('query parameter handling', () => {
72
+ it('should merge query parameters from both URLs', () => {
73
+ expect(mergeUrlsWithParams('https://example.com?a=1', 'login?b=2')).toBe('https://example.com/login/?a=1&b=2');
74
+ });
75
+ it('should override base query parameters with login query parameters', () => {
76
+ expect(mergeUrlsWithParams('https://example.com?param=1', 'login?param=2')).toBe('https://example.com/login/?param=2');
77
+ });
78
+ it('should handle multiple question marks in login URL', () => {
79
+ expect(mergeUrlsWithParams('https://example.com', 'login?param1=1?param2=2')).toBe('https://example.com/login/?param1=1%3Fparam2%3D2');
80
+ });
81
+ it('should handle complex paths and query parameters', () => {
82
+ expect(mergeUrlsWithParams('https://example.com/api?token=123', '/auth/login/?user=test&scope=full')).toBe('https://example.com/api/auth/login/?token=123&user=test&scope=full');
83
+ });
84
+ it('should handle alternative SSO host with relative path', () => {
85
+ const result = mergeUrlsWithParams('http://sso.app.client.com/auth?app=staging', '/users/login/?tracking=true&role=view');
86
+ const url = new URL(result);
87
+ expect(url.origin).toBe('http://sso.app.client.com');
88
+ expect(url.pathname).toBe('/auth/users/login/');
89
+ expect(url.searchParams.get('app')).toBe('staging');
90
+ expect(url.searchParams.get('tracking')).toBe('true');
91
+ expect(url.searchParams.get('role')).toBe('view');
92
+ });
93
+ });
94
+ });
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "Sisense",
12
12
  "Compose SDK"
13
13
  ],
14
- "version": "2.5.0",
14
+ "version": "2.7.0",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": "./dist/index.js"
@@ -1,2 +1,2 @@
1
1
  export { escapeSingleQuotes } from './escapeSingleQuotes.js';
2
- export { normalizeUrl } from './normalizeUrl.js';
2
+ export { mergeUrlsWithParams, normalizeUrl } from './url.js';
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { mergeUrlsWithParams, normalizeUrl } from './url.js';
4
+
5
+ describe('normalizeUrl', () => {
6
+ it("should add a trailing slash if url doesn't have it", () => {
7
+ expect(normalizeUrl('https://example.com')).toBe('https://example.com/');
8
+ });
9
+
10
+ it('should not add an extra trailing slash if url does have it', () => {
11
+ expect(normalizeUrl('https://example.com/')).toBe('https://example.com/');
12
+ });
13
+
14
+ it('should include search string when specified', () => {
15
+ expect(normalizeUrl('https://example.com?param=value', true)).toBe(
16
+ 'https://example.com/?param=value',
17
+ );
18
+ });
19
+
20
+ it('should throw error for invalid URL', () => {
21
+ expect(() => normalizeUrl('not-a-url')).toThrow();
22
+ });
23
+
24
+ it('should throw error for empty string', () => {
25
+ expect(() => normalizeUrl('')).toThrow('URL cannot be empty');
26
+ expect(() => normalizeUrl(' ')).toThrow('URL cannot be empty');
27
+ });
28
+
29
+ it('should throw error for non-string input', () => {
30
+ // @ts-expect-error Testing runtime type check
31
+ expect(() => normalizeUrl(null)).toThrow('URL must be a string');
32
+ // @ts-expect-error Testing runtime type check
33
+ expect(() => normalizeUrl(undefined)).toThrow('URL must be a string');
34
+ // @ts-expect-error Testing runtime type check
35
+ expect(() => normalizeUrl(123)).toThrow('URL must be a string');
36
+ });
37
+ });
38
+
39
+ describe('mergeUrlsWithParams', () => {
40
+ describe('input validation', () => {
41
+ it('should throw error when base URL is not provided', () => {
42
+ expect(() => mergeUrlsWithParams('')).toThrow('Base URL is required');
43
+ });
44
+
45
+ it('should throw error when base URL is invalid', () => {
46
+ expect(() => mergeUrlsWithParams('not-a-url')).toThrow('Base URL is not valid');
47
+ });
48
+ });
49
+
50
+ describe('base URL handling', () => {
51
+ it('should return base URL when login URL is not provided', () => {
52
+ expect(mergeUrlsWithParams('https://example.com')).toBe('https://example.com/');
53
+ });
54
+
55
+ it('should return base URL when login URL is just a slash', () => {
56
+ expect(mergeUrlsWithParams('https://example.com', '/')).toBe('https://example.com/');
57
+ });
58
+
59
+ it('should preserve base URL port if specified', () => {
60
+ expect(mergeUrlsWithParams('https://example.com:8080', 'login')).toBe(
61
+ 'https://example.com:8080/login/',
62
+ );
63
+ });
64
+ });
65
+
66
+ describe('absolute URL handling', () => {
67
+ it('should use absolute login URL when provided', () => {
68
+ expect(mergeUrlsWithParams('https://example.com', 'https://login.example.com/')).toBe(
69
+ 'https://login.example.com/',
70
+ );
71
+ });
72
+
73
+ it('should preserve query params in absolute login URL', () => {
74
+ expect(
75
+ mergeUrlsWithParams('https://example.com', 'https://login.example.com?token=123'),
76
+ ).toBe('https://login.example.com/?token=123');
77
+ });
78
+ });
79
+
80
+ describe('relative path handling', () => {
81
+ it('should merge relative login path with base URL', () => {
82
+ expect(mergeUrlsWithParams('https://example.com', 'login')).toBe(
83
+ 'https://example.com/login/',
84
+ );
85
+ });
86
+
87
+ it('should handle relative login path with leading slash', () => {
88
+ expect(mergeUrlsWithParams('https://example.com', '/login')).toBe(
89
+ 'https://example.com/login/',
90
+ );
91
+ });
92
+
93
+ it('should handle relative login path with trailing slash', () => {
94
+ expect(mergeUrlsWithParams('https://example.com', 'login/')).toBe(
95
+ 'https://example.com/login/',
96
+ );
97
+ });
98
+
99
+ it('should handle multiple slashes in path', () => {
100
+ expect(mergeUrlsWithParams('https://example.com', '//login///path//')).toBe(
101
+ 'https://example.com/login/path/',
102
+ );
103
+ });
104
+ });
105
+
106
+ describe('query parameter handling', () => {
107
+ it('should merge query parameters from both URLs', () => {
108
+ expect(mergeUrlsWithParams('https://example.com?a=1', 'login?b=2')).toBe(
109
+ 'https://example.com/login/?a=1&b=2',
110
+ );
111
+ });
112
+
113
+ it('should override base query parameters with login query parameters', () => {
114
+ expect(mergeUrlsWithParams('https://example.com?param=1', 'login?param=2')).toBe(
115
+ 'https://example.com/login/?param=2',
116
+ );
117
+ });
118
+
119
+ it('should handle multiple question marks in login URL', () => {
120
+ expect(mergeUrlsWithParams('https://example.com', 'login?param1=1?param2=2')).toBe(
121
+ 'https://example.com/login/?param1=1%3Fparam2%3D2',
122
+ );
123
+ });
124
+
125
+ it('should handle complex paths and query parameters', () => {
126
+ expect(
127
+ mergeUrlsWithParams(
128
+ 'https://example.com/api?token=123',
129
+ '/auth/login/?user=test&scope=full',
130
+ ),
131
+ ).toBe('https://example.com/api/auth/login/?token=123&user=test&scope=full');
132
+ });
133
+
134
+ it('should handle alternative SSO host with relative path', () => {
135
+ const result = mergeUrlsWithParams(
136
+ 'http://sso.app.client.com/auth?app=staging',
137
+ '/users/login/?tracking=true&role=view',
138
+ );
139
+ const url = new URL(result);
140
+
141
+ expect(url.origin).toBe('http://sso.app.client.com');
142
+ expect(url.pathname).toBe('/auth/users/login/');
143
+ expect(url.searchParams.get('app')).toBe('staging');
144
+ expect(url.searchParams.get('tracking')).toBe('true');
145
+ expect(url.searchParams.get('role')).toBe('view');
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Normalizes a URL string by ensuring it has a trailing slash and optionally includes search parameters.
3
+ * @param url - The URL string to normalize
4
+ * @param includeSearchString - Whether to include search parameters in the result
5
+ * @returns The normalized URL string
6
+ * @throws Error if the URL is invalid, empty, or not a string
7
+ */
8
+ export function normalizeUrl(url: string, includeSearchString?: boolean): string {
9
+ if (typeof url !== 'string') {
10
+ throw new Error('URL must be a string');
11
+ }
12
+
13
+ if (!url.trim()) {
14
+ throw new Error('URL cannot be empty');
15
+ }
16
+
17
+ try {
18
+ const urlObject = new URL(url);
19
+
20
+ return (
21
+ urlObject.origin +
22
+ (urlObject.pathname.endsWith('/') ? urlObject.pathname : urlObject.pathname + '/') +
23
+ (includeSearchString && urlObject.search ? urlObject.search : '')
24
+ );
25
+ } catch {
26
+ throw new Error(`Connection string ${url} is not a valid URL`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Merges two URLs, combining their paths and query parameters.
32
+ * If loginUrlStr is an absolute URL, it takes precedence over baseUrlStr.
33
+ * If loginUrlStr is relative, it's merged with baseUrlStr.
34
+ *
35
+ * @param baseUrlStr - The base URL to merge with
36
+ * @param loginUrlStr - The login URL (can be absolute or relative)
37
+ * @returns The merged URL string
38
+ * @throws Error if baseUrlStr is not a valid URL
39
+ */
40
+ export function mergeUrlsWithParams(baseUrlStr: string, loginUrlStr?: string): string {
41
+ if (!baseUrlStr) {
42
+ throw new Error('Base URL is required');
43
+ }
44
+
45
+ // Parse base URL first to validate it
46
+ let baseUrl: URL;
47
+ try {
48
+ baseUrl = new URL(baseUrlStr);
49
+ } catch {
50
+ throw new Error('Base URL is not valid');
51
+ }
52
+
53
+ // If no login URL provided or it's just a slash, return base URL
54
+ if (!loginUrlStr || loginUrlStr === '/') {
55
+ return baseUrl.toString();
56
+ }
57
+
58
+ try {
59
+ // Check if loginUrlStr is an absolute URL
60
+ const loginUrl = new URL(loginUrlStr);
61
+ return loginUrl.toString();
62
+ } catch {
63
+ // loginUrlStr is relative, proceed with merging
64
+ }
65
+
66
+ // Create a URL by joining base origin with login path
67
+ const resultUrl = new URL(baseUrl.toString());
68
+
69
+ // Split login URL into path and query parts
70
+ const [loginPath = '', loginQuery = ''] = loginUrlStr.split(/\?(.+)/);
71
+
72
+ // Handle the path merging
73
+ const basePath = baseUrl.pathname.replace(/\/$/, ''); // Remove trailing slash
74
+ const cleanLoginPath = loginPath
75
+ .replace(/^\/+/, '') // Remove leading slashes
76
+ .replace(/\/+$/, '') // Remove trailing slashes
77
+ .replace(/\/+/g, '/'); // Replace multiple slashes with single slash
78
+
79
+ resultUrl.pathname = cleanLoginPath ? `${basePath}/${cleanLoginPath}` : basePath;
80
+
81
+ // Parse and merge query parameters from both URLs
82
+ if (loginQuery) {
83
+ const loginParams = new URLSearchParams(loginQuery);
84
+ for (const [key, value] of loginParams.entries()) {
85
+ resultUrl.searchParams.set(key, value);
86
+ }
87
+ }
88
+
89
+ // Normalize the final URL by removing trailing slash
90
+ return normalizeUrl(resultUrl.toString(), true);
91
+ }