@intellegens/cornerstone-client 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -28,46 +28,76 @@ http://localhost:3000
28
28
 
29
29
  ## API reference
30
30
 
31
- ### Config Service
31
+ ### API Service
32
32
 
33
- This service provides way to load configuration for a Cornerstone client application.
33
+ This service provides way to load API configuration for a Cornerstone client application.
34
34
 
35
35
  ```ts
36
- import { Config } from './index.js';
37
- await configAPI.initialize();
36
+ import { ApiService } from '@intellegens/cornerstone-client';
37
+ const apiService = new ApiService();
38
+ await apiService.initialize();
38
39
  ```
39
40
 
40
- #### API base URL
41
+ or use the existing singleton instance
42
+
43
+ ```ts
44
+ import { apiService } from '@intellegens/cornerstone-client';
45
+ await apiService.initialize();
46
+ ```
47
+
48
+ #### API base URL detection
41
49
 
42
50
  Detected by checking the `CORNERSTONE-API-BASEURL` response header and the `websettings.json` file.
43
51
 
44
- - First, it checks the response headers of the current URL for the 'CORNERSTONE-API-BASEURL ' header.
52
+ - First, it checks the response headers of the current URL for the `CORNERSTONE-API-BASEURL` header.
45
53
  - If the header is not found, it attempts to load the `websettings.json` file and looks for an `api` field.
46
54
  - If no API base URL is found, it defaults to `undefined`.
47
55
 
56
+ ```ts
57
+ const fetchUrl = `${await apiService.getApiUrl('/my/endpoint')}`;
58
+ ```
59
+
48
60
  ### Auth Service
49
61
 
50
62
  This service provides authentication methods for managing user sessions in a Cornerstone client application.
51
63
 
52
- #### Usage
64
+ ```ts
65
+ import { AuthService } from './index.js';
66
+ const authService = new AuthService();
67
+ await authService.initialize();
68
+ const user = authService.user;
69
+ ```
53
70
 
54
- To use the Auth service, first create an instance by passing the API URL. Then, you can use the login, logout, and whoAmI methods to authenticate and manage the user session.
71
+ or use the existing singleton instance
55
72
 
56
73
  ```ts
57
- import { Auth } from './index.js';
58
- const authAPI = new Auth('https://your-api-url.com');
74
+ import { authService } from '@intellegens/cornerstone-client';
75
+ await authService.initialize();
76
+ const user = authService.user;
59
77
  ```
60
78
 
61
79
  #### Methods
62
80
 
63
- - login(username: string, password: string): Promise<User | undefined>
81
+ ##### Who Am I
64
82
 
65
- This method logs the user in by sending the provided username and password to the API. If the login is successful, it returns the authenticated User object.
83
+ ```ts
84
+ authService.whoAmI(): Promise<User | undefined>
85
+ ```
66
86
 
67
- - logout(): Promise<User | undefined>
87
+ This method checks the current session and returns the User object for the logged-in user. If no user is logged in, it returns undefined.
68
88
 
69
- This method logs the user out by sending a request to the API to end the session. It returns undefined if the logout is successful
89
+ ##### Sign in
70
90
 
71
- - whoAmI(): Promise<User | undefined>
91
+ ```ts
92
+ authService.signin(username: string, password: string): Promise<User | undefined>
93
+ ```
72
94
 
73
- This method checks the current session and returns the User object for the logged-in user. If no user is logged in, it returns undefined.
95
+ This method logs the user in by sending the provided username and password to the API. If the login is successful, it returns the authenticated User object.
96
+
97
+ ##### Sign out
98
+
99
+ ```ts
100
+ authService.logout(): Promise<User | undefined>
101
+ ```
102
+
103
+ This method logs the user out by sending a request to the API to end the session. It returns undefined if the logout is successful
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellegens/cornerstone-client",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Central API configuration service, fetches and exposes Cornerstone API configuration
3
+ *
4
+ * @export
5
+ * @class ApiService
6
+ */
7
+ export declare class ApiService {
8
+ /**
9
+ * Initializes the API configuration
10
+ *
11
+ * This method calls all methods that need to be called during the API configuration phase:
12
+ * - Starts detection of API base URL.
13
+ *
14
+ * @async
15
+ * @return {Promise<void>} Resolves when initialization is complete.
16
+ */
17
+ initialize(): Promise<void>;
18
+ private _apiBaseUrlPromise;
19
+ /**
20
+ * If API base URL detection was completed
21
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
22
+ */
23
+ _apiBaseUrlDetected: boolean;
24
+ /**
25
+ * If API base URL detection was completed, this property will hold the method by which detection was performed
26
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
27
+ */
28
+ _apiBaseUrlDetectionMethod: string | undefined;
29
+ /**
30
+ * If API base URL detection was completed, this property will hold the detected API base URL
31
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
32
+ */
33
+ _apiBaseUrl: string | undefined;
34
+ /**
35
+ * If API base URL detection has failed, this property will hold the error with which it has failed
36
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
37
+ */
38
+ _apiBaseError: Error | undefined;
39
+ /**
40
+ * Tries detecting API base URL by multiple methods
41
+ *
42
+ * - Checks the response headers of the current URL for the `CORNERSTONE-API-BASEURL` header.
43
+ * - Attempts to load the `websettings.json` file and looks for an `api` field.
44
+ * - Defaults to `undefined`.
45
+ *
46
+ * IMPORTANT: This method is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
47
+ *
48
+ * @async
49
+ * @return {Promise<string | undefined>} Returns detected API base URL
50
+ * @throws {Error} Throws error if either of the detection methods fails
51
+ */
52
+ _getApiBaseUrl(): Promise<string | undefined>;
53
+ /**
54
+ * Composes a full API URL for a provided endpoint path.
55
+ *
56
+ * @async
57
+ * @param relativeEndpointPath Relative endpoint path
58
+ * @return {Promise<string | undefined>} Returns the API endpoint's URL
59
+ * @throws {Error} Throws error if API base URL detection fails
60
+ */
61
+ getApiUrl(relativeEndpointPath: string): Promise<string>;
62
+ }
63
+ /**
64
+ * Singleton instance of ApiService
65
+ */
66
+ export declare const apiService: ApiService;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Central API configuration service, fetches and exposes Cornerstone API configuration
3
+ *
4
+ * @export
5
+ * @class ApiService
6
+ */
7
+ export class ApiService {
8
+ /**
9
+ * Initializes the API configuration
10
+ *
11
+ * This method calls all methods that need to be called during the API configuration phase:
12
+ * - Starts detection of API base URL.
13
+ *
14
+ * @async
15
+ * @return {Promise<void>} Resolves when initialization is complete.
16
+ */
17
+ async initialize() {
18
+ // (Pre)detect API base URL
19
+ await this._getApiBaseUrl();
20
+ }
21
+ // #region API Base URL detection
22
+ _apiBaseUrlPromise = undefined;
23
+ /**
24
+ * If API base URL detection was completed
25
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
26
+ */
27
+ _apiBaseUrlDetected = false;
28
+ /**
29
+ * If API base URL detection was completed, this property will hold the method by which detection was performed
30
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
31
+ */
32
+ _apiBaseUrlDetectionMethod = undefined;
33
+ /**
34
+ * If API base URL detection was completed, this property will hold the detected API base URL
35
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
36
+ */
37
+ _apiBaseUrl = undefined;
38
+ /**
39
+ * If API base URL detection has failed, this property will hold the error with which it has failed
40
+ * IMPORTANT: This property is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
41
+ */
42
+ _apiBaseError = undefined;
43
+ /**
44
+ * Tries detecting API base URL by multiple methods
45
+ *
46
+ * - Checks the response headers of the current URL for the `CORNERSTONE-API-BASEURL` header.
47
+ * - Attempts to load the `websettings.json` file and looks for an `api` field.
48
+ * - Defaults to `undefined`.
49
+ *
50
+ * IMPORTANT: This method is not meant to be used in normal cases - when ever possible use the .getApiUrl(relativeEndpointPath: string) method instead.
51
+ *
52
+ * @async
53
+ * @return {Promise<string | undefined>} Returns detected API base URL
54
+ * @throws {Error} Throws error if either of the detection methods fails
55
+ */
56
+ async _getApiBaseUrl() {
57
+ // Check if API already detected; don't reattempt detection
58
+ if (this._apiBaseUrlDetected)
59
+ return this._apiBaseUrl;
60
+ // Check if detection already in progress; don't allow multiple detections in parallel
61
+ if (this._apiBaseUrlPromise)
62
+ return this._apiBaseUrlPromise;
63
+ // Detect API base URL
64
+ return (this._apiBaseUrlPromise = new Promise((resolve, reject) => (async () => {
65
+ // Reset
66
+ this._apiBaseUrlDetected = false;
67
+ this._apiBaseUrlDetectionMethod = undefined;
68
+ this._apiBaseUrl = undefined;
69
+ this._apiBaseError = undefined;
70
+ // Check CORNERSTONE-API-BASEURL header for API base URL
71
+ try {
72
+ const currentUrlResponse = await fetch(window.location.href, { method: 'GET' });
73
+ const apiHeader = currentUrlResponse.headers.get('CORNERSTONE-API-BASEURL');
74
+ if (apiHeader) {
75
+ this._apiBaseUrlDetected = true;
76
+ this._apiBaseUrlDetectionMethod = 'GET / Header:CORNERSTONE-API-BASEURL';
77
+ this._apiBaseUrl = apiHeader;
78
+ this._apiBaseError = undefined;
79
+ this._apiBaseUrlPromise = undefined;
80
+ return resolve(this._apiBaseUrl);
81
+ }
82
+ }
83
+ catch (err) {
84
+ this._apiBaseError = err instanceof Error ? err : new Error('Failed loading API base URL from response header!');
85
+ }
86
+ // Check ./websettings.json header for API base URL
87
+ try {
88
+ const webSettingsResponse = await fetch('./websettings.json', { method: 'GET' });
89
+ if (webSettingsResponse.status === 404)
90
+ return (this._apiBaseUrl = undefined);
91
+ const webSettings = await webSettingsResponse.json();
92
+ if (webSettings?.api) {
93
+ this._apiBaseUrlDetected = true;
94
+ this._apiBaseUrlDetectionMethod = 'GET websettings.json';
95
+ this._apiBaseUrl = webSettings.api;
96
+ this._apiBaseError = undefined;
97
+ this._apiBaseUrlPromise = undefined;
98
+ return resolve(this._apiBaseUrl);
99
+ }
100
+ }
101
+ catch (err) {
102
+ this._apiBaseError = err instanceof Error ? err : new Error('Failed loading API base URL from settings file!');
103
+ }
104
+ // Check if error caught during any of the detection methods
105
+ if (this._apiBaseError !== undefined) {
106
+ this._apiBaseUrlDetected = false;
107
+ this._apiBaseUrlDetectionMethod = undefined;
108
+ this._apiBaseUrl = undefined;
109
+ this._apiBaseUrlPromise = undefined;
110
+ return reject(this._apiBaseError);
111
+ }
112
+ // Default to no API found
113
+ this._apiBaseUrlDetected = true;
114
+ this._apiBaseUrlDetectionMethod = undefined;
115
+ this._apiBaseUrl = undefined;
116
+ this._apiBaseError = undefined;
117
+ this._apiBaseUrlPromise = undefined;
118
+ return resolve(this._apiBaseUrl);
119
+ })()));
120
+ }
121
+ /**
122
+ * Composes a full API URL for a provided endpoint path.
123
+ *
124
+ * @async
125
+ * @param relativeEndpointPath Relative endpoint path
126
+ * @return {Promise<string | undefined>} Returns the API endpoint's URL
127
+ * @throws {Error} Throws error if API base URL detection fails
128
+ */
129
+ async getApiUrl(relativeEndpointPath) {
130
+ const apiBaseUrl = await this._getApiBaseUrl();
131
+ const domain = !apiBaseUrl ? `` : apiBaseUrl?.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
132
+ const path = !relativeEndpointPath.startsWith('/') ? relativeEndpointPath : relativeEndpointPath.substring(1);
133
+ return `${domain}${path}`;
134
+ }
135
+ }
136
+ /**
137
+ * Singleton instance of ApiService
138
+ */
139
+ export const apiService = new ApiService();
@@ -1,46 +1,49 @@
1
1
  import { User, UserInfo } from './types';
2
2
  export { User, UserInfo };
3
3
  /**
4
- * Auth class is responsible for managing user authentication operations.
4
+ * AuthService class is responsible for managing user authentication operations.
5
5
  * It supports login, logout, and checking the current user's details.
6
6
  *
7
7
  * @export
8
- * @class Auth
8
+ * @class AuthService
9
9
  */
10
- export declare class Auth<TId, TUser = User<TId>> {
11
- private _apiUrl;
10
+ export declare class AuthService<TId, TUser = User<TId>> {
11
+ /**
12
+ * Holds currently authenticated user's details
13
+ */
12
14
  user: TUser | undefined;
13
15
  /**
14
- * Creates an instance of Auth.
16
+ * Initializes the Authentication status by checking with the API if the current user is authenticated or not
15
17
  *
16
- * @param {string} apiUrl
17
- * @memberof Auth
18
+ * @async
19
+ * @return {Promise<void>} Resolves when initialization is complete.
18
20
  */
19
- constructor(apiUrl: string);
21
+ initialize(): Promise<void>;
20
22
  /**
21
- * Retrieves the current user's details if they are logged in.
23
+ * Checks if user is authenticated and retrieves the current user's details if they are
22
24
  *
23
- * @return {Promise<TUser>} Currently logged in user
25
+ * @return {Promise<TUser>} Currently logged in user's details
24
26
  * @throws {Error} Error if fetch fails
25
- * @memberof Auth
26
27
  */
27
28
  whoAmI(): Promise<TUser>;
28
29
  /**
29
- * Logs in a user using their username and password, and retrieves the user data.
30
+ * Authenticates a user using their username and password, and retrieves user's details
30
31
  *
31
32
  * @param {string} username Login credentials: username
32
33
  * @param {string} password Login credentials: password
33
- * @return {Promise<TUser>} Currently logged in user
34
+ * @return {Promise<TUser>} Currently logged in user's details
34
35
  * @throws {Error} Error if fetch fails
35
- * @memberof Auth
36
36
  */
37
- login(username: string, password: string): Promise<TUser>;
37
+ signin(username: string, password: string): Promise<TUser>;
38
38
  /**
39
- * Logs out the current user and clears their authentication state.
39
+ * Deauthenticates current user
40
40
  *
41
41
  * @return {Promise<void>}
42
42
  * @throws {Error} Error if fetch fails
43
- * @memberof Auth
44
43
  */
45
- logout(): Promise<void>;
44
+ signout(): Promise<void>;
46
45
  }
46
+ /**
47
+ * Singleton instance of AuthService
48
+ */
49
+ export declare const authService: AuthService<unknown, User<unknown>>;
@@ -1,32 +1,36 @@
1
+ import { apiService } from '../api';
1
2
  /**
2
- * Auth class is responsible for managing user authentication operations.
3
+ * AuthService class is responsible for managing user authentication operations.
3
4
  * It supports login, logout, and checking the current user's details.
4
5
  *
5
6
  * @export
6
- * @class Auth
7
+ * @class AuthService
7
8
  */
8
- export class Auth {
9
- _apiUrl;
9
+ export class AuthService {
10
+ /**
11
+ * Holds currently authenticated user's details
12
+ */
10
13
  user = undefined;
11
14
  /**
12
- * Creates an instance of Auth.
15
+ * Initializes the Authentication status by checking with the API if the current user is authenticated or not
13
16
  *
14
- * @param {string} apiUrl
15
- * @memberof Auth
17
+ * @async
18
+ * @return {Promise<void>} Resolves when initialization is complete.
16
19
  */
17
- constructor(apiUrl) {
18
- this._apiUrl = apiUrl;
20
+ async initialize() {
21
+ // Check user's authentication status
22
+ await this.whoAmI();
19
23
  }
20
24
  /**
21
- * Retrieves the current user's details if they are logged in.
25
+ * Checks if user is authenticated and retrieves the current user's details if they are
22
26
  *
23
- * @return {Promise<TUser>} Currently logged in user
27
+ * @return {Promise<TUser>} Currently logged in user's details
24
28
  * @throws {Error} Error if fetch fails
25
- * @memberof Auth
26
29
  */
27
30
  async whoAmI() {
28
31
  try {
29
- const response = await fetch(`${this._apiUrl}/auth/who-am-i`, { credentials: 'include' });
32
+ const url = await apiService.getApiUrl('/auth/whoami');
33
+ const response = await fetch(url, { credentials: 'include' });
30
34
  if (response.ok) {
31
35
  const info = await response.json();
32
36
  return (this.user = info.user);
@@ -40,17 +44,17 @@ export class Auth {
40
44
  }
41
45
  }
42
46
  /**
43
- * Logs in a user using their username and password, and retrieves the user data.
47
+ * Authenticates a user using their username and password, and retrieves user's details
44
48
  *
45
49
  * @param {string} username Login credentials: username
46
50
  * @param {string} password Login credentials: password
47
- * @return {Promise<TUser>} Currently logged in user
51
+ * @return {Promise<TUser>} Currently logged in user's details
48
52
  * @throws {Error} Error if fetch fails
49
- * @memberof Auth
50
53
  */
51
- async login(username, password) {
54
+ async signin(username, password) {
52
55
  try {
53
- const response = await fetch(`${this._apiUrl}/auth/signin`, {
56
+ const url = await apiService.getApiUrl('/auth/signin');
57
+ const response = await fetch(url, {
54
58
  method: 'POST',
55
59
  headers: { 'Content-Type': 'application/json' },
56
60
  body: JSON.stringify({ username, password }),
@@ -60,33 +64,37 @@ export class Auth {
60
64
  return (this.user = await this.whoAmI());
61
65
  }
62
66
  else {
63
- throw new Error('Failed user login');
67
+ throw new Error('Failed signing in');
64
68
  }
65
69
  }
66
70
  catch (error) {
67
- throw error instanceof Error ? error : new Error('Failed user login');
71
+ throw error instanceof Error ? error : new Error('Failed signing in');
68
72
  }
69
73
  }
70
74
  /**
71
- * Logs out the current user and clears their authentication state.
75
+ * Deauthenticates current user
72
76
  *
73
77
  * @return {Promise<void>}
74
78
  * @throws {Error} Error if fetch fails
75
- * @memberof Auth
76
79
  */
77
- async logout() {
80
+ async signout() {
78
81
  try {
79
- const response = await fetch(`${this._apiUrl}/auth/logout`, { credentials: 'include' });
82
+ const url = await apiService.getApiUrl('/auth/signout');
83
+ const response = await fetch(url, { credentials: 'include' });
80
84
  if (response.ok) {
81
85
  this.user = undefined;
82
86
  return;
83
87
  }
84
88
  else {
85
- throw new Error('Failed user logout');
89
+ throw new Error('Failed signing out');
86
90
  }
87
91
  }
88
92
  catch (error) {
89
- throw error instanceof Error ? error : new Error('Failed user logout');
93
+ throw error instanceof Error ? error : new Error('Failed signing out');
90
94
  }
91
95
  }
92
96
  }
97
+ /**
98
+ * Singleton instance of AuthService
99
+ */
100
+ export const authService = new AuthService();
@@ -7,10 +7,6 @@ import { User } from './user';
7
7
  *
8
8
  * @param {TUser} user The user object containing user details.
9
9
  * @param {Date} accessTokenExpiry The date and time when the access token will expire.
10
- *
11
- * @typedef {Object} UserInfo
12
- * @property {TUser} user The user object.
13
- * @property {Date} accessTokenExpiry The expiry date of the access token.
14
10
  */
15
11
  export type UserInfo<TId, TUser = User<TId>> = {
16
12
  user: TUser;
@@ -5,10 +5,6 @@
5
5
  *
6
6
  * @param {TId} id The unique identifier for the user.
7
7
  * @param {string} email The email address associated with the user.
8
- *
9
- * @typedef {Object} User
10
- * @property {TId} id The unique identifier of the user.
11
- * @property {string} email The email address of the user.
12
8
  */
13
9
  export type User<TId> = {
14
10
  id: TId;
@@ -1,17 +1,10 @@
1
1
  /**
2
- * Configuration class for managing the API
2
+ * Central configuration service, fetches and exposes Cornerstone configuration
3
3
  *
4
4
  * @export
5
- * @class Config
5
+ * @class ConfigService
6
6
  */
7
- export declare class Config {
8
- /**
9
- * The base URL of the API. It is undefined by default and gets populated through the initialization process.
10
- *
11
- * @type {(string | undefined)}
12
- * @memberof Config
13
- */
14
- apiBaseUrl: string | undefined;
7
+ export declare class ConfigService {
15
8
  /**
16
9
  * Initializes the configuration by detecting the API base URL.
17
10
  *
@@ -19,19 +12,36 @@ export declare class Config {
19
12
  *
20
13
  * @async
21
14
  * @return {Promise<void>} Resolves when the API base URL is detected.
22
- * @memberof Config
23
15
  */
24
16
  initialize(): Promise<void>;
17
+ private _apiBaseUrlDetected;
18
+ private _apiBaseUrlDetectionMethod;
19
+ private _apiBaseUrl;
20
+ private _apiBaseError;
21
+ private _apiBaseUrlPromise;
25
22
  /**
26
23
  * Detects the API base URL by checking the `CORNERSTONE-API-BASEURL` header and the `websettings.json` file.
27
24
  *
28
- * First, it checks the response headers of the current URL for the 'CORNERSTONE-API-BASEURL' header.
29
- * If the header is not found, it attempts to load the `websettings.json` file and looks for an `api` field.
30
- * If no API base URL is found, it defaults to `undefined`.
25
+ * - First, it checks the response headers of the current URL for the `CORNERSTONE-API-BASEURL` header.
26
+ * - If the header is not found, it attempts to load the `websettings.json` file and looks for an `api` field.
27
+ * - If no API base URL is found, it defaults to `undefined`.
31
28
  *
29
+ * @async
32
30
  * @return {Promise<string | undefined>} Returns the API base URL
33
- * @throws {Error} Throws error if fetch fails
34
- * @memberof Config
31
+ * @throws {Error} Throws error if either of the detection methods fails
32
+ */
33
+ _getApiBaseUrl(): Promise<string | undefined>;
34
+ /**
35
+ * Returns a full URL for a provided endpoint path.
36
+ *
37
+ * @async
38
+ * @param relativeEndpointPath Relative endpoint path
39
+ * @return {Promise<string | undefined>} Returns the API endpoint's URL
40
+ * @throws {Error} Throws error if API base URL detection fails
35
41
  */
36
- detectApiBaseUrl(): Promise<string | undefined>;
42
+ getApiUrl(relativeEndpointPath: string): Promise<string>;
37
43
  }
44
+ /**
45
+ * Singleton instance of ConfigService
46
+ */
47
+ export declare const configService: ConfigService;
@@ -1,17 +1,10 @@
1
1
  /**
2
- * Configuration class for managing the API
2
+ * Central configuration service, fetches and exposes Cornerstone configuration
3
3
  *
4
4
  * @export
5
- * @class Config
5
+ * @class ConfigService
6
6
  */
7
- export class Config {
8
- /**
9
- * The base URL of the API. It is undefined by default and gets populated through the initialization process.
10
- *
11
- * @type {(string | undefined)}
12
- * @memberof Config
13
- */
14
- apiBaseUrl = undefined;
7
+ export class ConfigService {
15
8
  /**
16
9
  * Initializes the configuration by detecting the API base URL.
17
10
  *
@@ -19,51 +12,109 @@ export class Config {
19
12
  *
20
13
  * @async
21
14
  * @return {Promise<void>} Resolves when the API base URL is detected.
22
- * @memberof Config
23
15
  */
24
16
  async initialize() {
25
- await this.detectApiBaseUrl();
17
+ // (Pre)detect API base URL
18
+ await this._getApiBaseUrl();
26
19
  }
20
+ // #region API Base URL detection
21
+ _apiBaseUrlDetected = false;
22
+ _apiBaseUrlDetectionMethod = undefined;
23
+ _apiBaseUrl = undefined;
24
+ _apiBaseError = undefined;
25
+ _apiBaseUrlPromise = undefined;
27
26
  /**
28
27
  * Detects the API base URL by checking the `CORNERSTONE-API-BASEURL` header and the `websettings.json` file.
29
28
  *
30
- * First, it checks the response headers of the current URL for the 'CORNERSTONE-API-BASEURL' header.
31
- * If the header is not found, it attempts to load the `websettings.json` file and looks for an `api` field.
32
- * If no API base URL is found, it defaults to `undefined`.
29
+ * - First, it checks the response headers of the current URL for the `CORNERSTONE-API-BASEURL` header.
30
+ * - If the header is not found, it attempts to load the `websettings.json` file and looks for an `api` field.
31
+ * - If no API base URL is found, it defaults to `undefined`.
33
32
  *
33
+ * @async
34
34
  * @return {Promise<string | undefined>} Returns the API base URL
35
- * @throws {Error} Throws error if fetch fails
36
- * @memberof Config
35
+ * @throws {Error} Throws error if either of the detection methods fails
37
36
  */
38
- async detectApiBaseUrl() {
39
- // Initialize a globally tracked error
40
- let error = undefined;
41
- // Check CORNERSTONE-API-BASEURL header for API base URL
42
- try {
43
- const currentUrlResponse = await fetch(window.location.href, { method: 'GET' });
44
- const apiHeader = currentUrlResponse.headers.get('CORNERSTONE-API-BASEURL');
45
- if (apiHeader)
46
- return (this.apiBaseUrl = apiHeader);
47
- }
48
- catch (err) {
49
- error = err instanceof Error ? err : new Error('Failed loading configuration from response header!');
50
- }
51
- // Check ./websettings.json header for API base URL
52
- try {
53
- const webSettingsResponse = await fetch('./websettings.json', { method: 'GET' });
54
- if (webSettingsResponse.status === 404)
55
- return (this.apiBaseUrl = undefined);
56
- const webSettings = await webSettingsResponse.json();
57
- if (webSettings?.api)
58
- return (this.apiBaseUrl = webSettings.api);
59
- }
60
- catch (err) {
61
- error = err instanceof Error ? err : new Error('Failed loading configuration from settings file!');
62
- }
63
- // Check if error caught
64
- if (error !== undefined)
65
- throw error;
66
- // Default to no API found
67
- return (this.apiBaseUrl = undefined);
37
+ async _getApiBaseUrl() {
38
+ // Check if API already detected; don't reattempt detection
39
+ if (this._apiBaseUrlDetected)
40
+ return this._apiBaseUrl;
41
+ // Check if detection already in progress; don't allow multiple detections in parallel
42
+ if (this._apiBaseUrlPromise)
43
+ return this._apiBaseUrlPromise;
44
+ // Detect API base URL
45
+ return (this._apiBaseUrlPromise = new Promise((resolve, reject) => (async () => {
46
+ // Reset
47
+ this._apiBaseUrlDetected = false;
48
+ this._apiBaseUrlDetectionMethod = undefined;
49
+ this._apiBaseUrl = undefined;
50
+ this._apiBaseError = undefined;
51
+ // Check CORNERSTONE-API-BASEURL header for API base URL
52
+ try {
53
+ const currentUrlResponse = await fetch(window.location.href, { method: 'GET' });
54
+ const apiHeader = currentUrlResponse.headers.get('CORNERSTONE-API-BASEURL');
55
+ if (apiHeader) {
56
+ this._apiBaseUrlDetected = true;
57
+ this._apiBaseUrlDetectionMethod = 'GET / Header:CORNERSTONE-API-BASEURL';
58
+ this._apiBaseUrl = apiHeader;
59
+ this._apiBaseError = undefined;
60
+ this._apiBaseUrlPromise = undefined;
61
+ return resolve(this._apiBaseUrl);
62
+ }
63
+ }
64
+ catch (err) {
65
+ this._apiBaseError = err instanceof Error ? err : new Error('Failed loading configuration from response header!');
66
+ }
67
+ // Check ./websettings.json header for API base URL
68
+ try {
69
+ const webSettingsResponse = await fetch('./websettings.json', { method: 'GET' });
70
+ if (webSettingsResponse.status === 404)
71
+ return (this._apiBaseUrl = undefined);
72
+ const webSettings = await webSettingsResponse.json();
73
+ if (webSettings?.api) {
74
+ this._apiBaseUrlDetected = true;
75
+ this._apiBaseUrlDetectionMethod = 'GET websettings.json';
76
+ this._apiBaseUrl = webSettings.api;
77
+ this._apiBaseError = undefined;
78
+ this._apiBaseUrlPromise = undefined;
79
+ return resolve(this._apiBaseUrl);
80
+ }
81
+ }
82
+ catch (err) {
83
+ this._apiBaseError = err instanceof Error ? err : new Error('Failed loading configuration from settings file!');
84
+ }
85
+ // Check if error caught during any of the detection methods
86
+ if (this._apiBaseError !== undefined) {
87
+ this._apiBaseUrlDetected = false;
88
+ this._apiBaseUrlDetectionMethod = undefined;
89
+ this._apiBaseUrl = undefined;
90
+ this._apiBaseUrlPromise = undefined;
91
+ return reject(this._apiBaseError);
92
+ }
93
+ // Default to no API found
94
+ this._apiBaseUrlDetected = true;
95
+ this._apiBaseUrlDetectionMethod = undefined;
96
+ this._apiBaseUrl = undefined;
97
+ this._apiBaseError = undefined;
98
+ this._apiBaseUrlPromise = undefined;
99
+ return resolve(this._apiBaseUrl);
100
+ })()));
101
+ }
102
+ /**
103
+ * Returns a full URL for a provided endpoint path.
104
+ *
105
+ * @async
106
+ * @param relativeEndpointPath Relative endpoint path
107
+ * @return {Promise<string | undefined>} Returns the API endpoint's URL
108
+ * @throws {Error} Throws error if API base URL detection fails
109
+ */
110
+ async getApiUrl(relativeEndpointPath) {
111
+ const apiBaseUrl = await this._getApiBaseUrl();
112
+ const domain = !apiBaseUrl ? `` : apiBaseUrl?.endsWith('/') ? apiBaseUrl : `${apiBaseUrl}/`;
113
+ const path = !relativeEndpointPath.startsWith('/') ? relativeEndpointPath : relativeEndpointPath.substring(1);
114
+ return `${domain}${path}`;
68
115
  }
69
116
  }
117
+ /**
118
+ * Singleton instance of ConfigService
119
+ */
120
+ export const configService = new ConfigService();
@@ -1,2 +1,2 @@
1
- export * from './config';
1
+ export * from './api';
2
2
  export * from './auth';
package/services/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from './config';
1
+ export * from './api';
2
2
  export * from './auth';