@etsoo/appscript 1.5.20 → 1.5.22

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.
@@ -88,6 +88,13 @@ class CoreAppTest extends CoreApp<
88
88
  */
89
89
  endpoint: 'http://{hostname}/com.etsoo.SmartERPApi/api/',
90
90
 
91
+ endpoints: {
92
+ core: {
93
+ endpoint: 'http://{hostname}:9001/api/',
94
+ webUrl: ''
95
+ }
96
+ },
97
+
91
98
  /**
92
99
  * App root url
93
100
  */
@@ -155,6 +162,16 @@ const appClass = EnhanceApp(CoreAppTest);
155
162
  const app = new appClass();
156
163
  app.changeCulture(app.settings.cultures[0]);
157
164
 
165
+ test('Test for domain replacement', () => {
166
+ expect(app.settings.endpoint).toBe(
167
+ 'http://localhost/com.etsoo.SmartERPApi/api/'
168
+ );
169
+
170
+ expect(app.settings.endpoints?.core.endpoint).toBe(
171
+ 'http://localhost:9001/api/'
172
+ );
173
+ });
174
+
158
175
  test('Test for properties', () => {
159
176
  expect(app.settings.currentRegion.label).toBe('中国大陆');
160
177
  });
@@ -9,6 +9,10 @@ import { IUser } from '../state/User';
9
9
  import { IAppSettings } from './AppSettings';
10
10
  import { FormatResultCustomCallback, IApp, IAppFields, IDetectIPCallback, NavigateOptions, RefreshTokenProps, RefreshTokenResult } from './IApp';
11
11
  import { UserRole } from './UserRole';
12
+ import { ExternalEndpoint } from './ExternalSettings';
13
+ import { ApiRefreshTokenDto } from '../erp/dto/ApiRefreshTokenDto';
14
+ type ApiRefreshTokenFunction = (api: IApi, token: string) => Promise<[string, number] | undefined>;
15
+ type ApiTaskData = [IApi, number, number, ApiRefreshTokenFunction, string?];
12
16
  /**
13
17
  * Core application interface
14
18
  */
@@ -119,15 +123,16 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
119
123
  */
120
124
  get isReady(): boolean;
121
125
  private set isReady(value);
126
+ /**
127
+ * Current cached URL
128
+ */
129
+ get cachedUrl(): string | undefined | null;
130
+ set cachedUrl(value: string | undefined | null);
122
131
  private _isTryingLogin;
123
132
  /**
124
133
  * Last called with token refresh
125
134
  */
126
135
  protected lastCalled: boolean;
127
- /**
128
- * Token refresh count down seed
129
- */
130
- protected refreshCountdownSeed: number;
131
136
  /**
132
137
  * Init call Api URL
133
138
  */
@@ -137,6 +142,8 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
137
142
  */
138
143
  protected passphrase: string;
139
144
  private cachedRefreshToken?;
145
+ private apis;
146
+ private tasks;
140
147
  /**
141
148
  * Get persisted fields
142
149
  */
@@ -150,7 +157,7 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
150
157
  * @param name Application name
151
158
  * @param debug Debug mode
152
159
  */
153
- protected constructor(settings: S, api: IApi, notifier: INotifier<N, C>, storage: IStorage, name: string, debug?: boolean);
160
+ protected constructor(settings: S, api: IApi | undefined | null, notifier: INotifier<N, C>, storage: IStorage, name: string, debug?: boolean);
154
161
  private getDeviceId;
155
162
  private resetKeys;
156
163
  /**
@@ -178,11 +185,32 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
178
185
  * Persist settings to source when application exit
179
186
  */
180
187
  persist(): void;
188
+ /**
189
+ * Add scheduled task
190
+ * @param task Task, return false to stop
191
+ * @param seconds Interval in seconds
192
+ */
193
+ addTask(task: () => PromiseLike<void | false>, seconds: number): void;
194
+ /**
195
+ * Create API client, override to implement custom client creation by name
196
+ * @param name Client name
197
+ * @param item External endpoint item
198
+ * @returns Result
199
+ */
200
+ createApi(name: string, item: ExternalEndpoint, refresh?: (api: IApi, token: string) => Promise<[string, number] | undefined>): IApi<any>;
201
+ /**
202
+ * Update API token and expires
203
+ * @param name Api name
204
+ * @param token Refresh token
205
+ * @param seconds Access token expires in seconds
206
+ */
207
+ updateApi(name: string, token: string | undefined, seconds: number): void;
208
+ updateApi(data: ApiTaskData, token: string | undefined, seconds: number): void;
181
209
  /**
182
210
  * Setup Api
183
211
  * @param api Api
184
212
  */
185
- protected setApi(api: IApi): void;
213
+ protected setApi(api: IApi, refresh?: ApiRefreshTokenFunction): void;
186
214
  /**
187
215
  * Setup Api error handler
188
216
  * @param api Api
@@ -535,12 +563,6 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
535
563
  * Callback where exit a page
536
564
  */
537
565
  pageExit(): void;
538
- /**
539
- * Refresh countdown
540
- * @param seconds Seconds
541
- */
542
- protected refreshCountdown(seconds: number): void;
543
- protected refreshCountdownClear(): void;
544
566
  /**
545
567
  * Fresh countdown UI
546
568
  * @param callback Callback
@@ -555,6 +577,35 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
555
577
  * Setup callback
556
578
  */
557
579
  setup(): void;
580
+ /**
581
+ * Exchange token data
582
+ * @param api API
583
+ * @param token Core system's refresh token to exchange
584
+ * @returns Result
585
+ */
586
+ exchangeToken(api: IApi, token: string): Promise<void>;
587
+ /**
588
+ * Exchange token update, override to get the new token
589
+ * @param api API
590
+ * @param data API refresh token data
591
+ */
592
+ protected exchangeTokenUpdate(api: IApi, data: ApiRefreshTokenDto): void;
593
+ /**
594
+ * Exchange intergration tokens for all APIs
595
+ * @param token Core system's refresh token to exchange
596
+ */
597
+ exchangeTokenAll(token: string): void;
598
+ /**
599
+ * API refresh token
600
+ * @param api Current API
601
+ * @param token Refresh token
602
+ * @returns Result
603
+ */
604
+ protected apiRefreshToken(api: IApi, token: string): Promise<[string, number] | undefined>;
605
+ /**
606
+ * Setup tasks
607
+ */
608
+ protected setupTasks(): void;
558
609
  /**
559
610
  * Signout, with userLogout and toLoginPage
560
611
  * @param apiUrl Signout API URL
@@ -597,3 +648,4 @@ export declare abstract class CoreApp<U extends IUser, S extends IAppSettings, N
597
648
  */
598
649
  warning(message: NotificationContent<N>, align?: NotificationAlign): void;
599
650
  }
651
+ export {};
@@ -83,6 +83,15 @@ class CoreApp {
83
83
  set isReady(value) {
84
84
  this._isReady = value;
85
85
  }
86
+ /**
87
+ * Current cached URL
88
+ */
89
+ get cachedUrl() {
90
+ return this.storage.getData(this.fields.cachedUrl);
91
+ }
92
+ set cachedUrl(value) {
93
+ this.storage.setData(this.fields.cachedUrl, value);
94
+ }
86
95
  /**
87
96
  * Get persisted fields
88
97
  */
@@ -115,10 +124,6 @@ class CoreApp {
115
124
  * Last called with token refresh
116
125
  */
117
126
  this.lastCalled = false;
118
- /**
119
- * Token refresh count down seed
120
- */
121
- this.refreshCountdownSeed = 0;
122
127
  /**
123
128
  * Init call Api URL
124
129
  */
@@ -127,6 +132,8 @@ class CoreApp {
127
132
  * Passphrase for encryption
128
133
  */
129
134
  this.passphrase = '';
135
+ this.apis = {};
136
+ this.tasks = [];
130
137
  if (settings?.regions?.length === 0) {
131
138
  throw new Error('No regions defined');
132
139
  }
@@ -136,7 +143,30 @@ class CoreApp {
136
143
  throw new Error('No default region defined');
137
144
  }
138
145
  this.defaultRegion = region;
139
- this.api = api;
146
+ const refresh = async (api, token) => {
147
+ if (this.lastCalled) {
148
+ // Call refreshToken to update access token
149
+ await this.refreshToken();
150
+ }
151
+ else {
152
+ // Popup countdown for user action
153
+ this.freshCountdownUI();
154
+ }
155
+ return undefined;
156
+ };
157
+ if (api) {
158
+ // Base URL of the API
159
+ api.baseUrl = this.settings.endpoint;
160
+ api.name = 'system';
161
+ this.setApi(api, refresh);
162
+ this.api = api;
163
+ }
164
+ else {
165
+ this.api = this.createApi('system', {
166
+ endpoint: settings.endpoint,
167
+ webUrl: settings.webUrl
168
+ }, refresh);
169
+ }
140
170
  this.notifier = notifier;
141
171
  this.storage = storage;
142
172
  this.name = name;
@@ -145,7 +175,6 @@ class CoreApp {
145
175
  this.fields = IApp_1.appFields.reduce((a, v) => ({ ...a, [v]: 'smarterp-' + v + '-' + name }), {});
146
176
  // Device id
147
177
  this._deviceId = storage.getData(this.fields.deviceId, '');
148
- this.setApi(api);
149
178
  const { currentCulture, currentRegion } = settings;
150
179
  // Load resources
151
180
  Promise.all([loadCrypto(), this.changeCulture(currentCulture)]).then(([cj, _resources]) => {
@@ -263,17 +292,56 @@ class CoreApp {
263
292
  return;
264
293
  this.storage.copyTo(this.persistedFields);
265
294
  }
295
+ /**
296
+ * Add scheduled task
297
+ * @param task Task, return false to stop
298
+ * @param seconds Interval in seconds
299
+ */
300
+ addTask(task, seconds) {
301
+ this.tasks.push([task, seconds, seconds]);
302
+ }
303
+ /**
304
+ * Create API client, override to implement custom client creation by name
305
+ * @param name Client name
306
+ * @param item External endpoint item
307
+ * @returns Result
308
+ */
309
+ createApi(name, item, refresh) {
310
+ if (this.apis[name] != null) {
311
+ throw new Error(`API ${name} already exists`);
312
+ }
313
+ const api = (0, restclient_1.createClient)();
314
+ api.name = name;
315
+ api.baseUrl = item.endpoint;
316
+ this.setApi(api, refresh);
317
+ return api;
318
+ }
319
+ updateApi(nameOrData, token, seconds) {
320
+ const api = typeof nameOrData === 'string' ? this.apis[nameOrData] : nameOrData;
321
+ if (api == null)
322
+ return;
323
+ // Consider the API call delay
324
+ if (seconds > 0) {
325
+ seconds -= 30;
326
+ if (seconds < 10)
327
+ seconds = 10;
328
+ }
329
+ api[1] = seconds;
330
+ api[2] = seconds;
331
+ api[4] = token;
332
+ }
266
333
  /**
267
334
  * Setup Api
268
335
  * @param api Api
269
336
  */
270
- setApi(api) {
271
- // Base URL of the API
272
- api.baseUrl = this.settings.endpoint;
337
+ setApi(api, refresh) {
273
338
  // onRequest, show loading or not, rewrite the property to override default action
274
339
  this.setApiLoading(api);
275
340
  // Global API error handler
276
341
  this.setApiErrorHandler(api);
342
+ // Setup API countdown
343
+ refresh ?? (refresh = this.apiRefreshToken.bind(this));
344
+ this.apis[api.name] = [api, -1, -1, refresh];
277
345
  }
278
346
  /**
279
347
  * Setup Api error handler
@@ -284,7 +352,7 @@ class CoreApp {
284
352
  api.onError = (error) => {
285
353
  // Debug
286
354
  if (this.debug) {
287
- console.debug('CoreApp.setApiErrorHandler', api, error, handlerFor401);
355
+ console.debug(`CoreApp.${this.name}.setApiErrorHandler`, api, error, handlerFor401);
288
356
  }
289
357
  // Error code
290
358
  const status = error.response
@@ -306,12 +374,12 @@ class CoreApp {
306
374
  (error.message === 'Network Error' ||
307
375
  error.message === 'Failed to fetch')) {
308
376
  // Network error
309
- this.notifier.alert(this.get('networkError'));
377
+ this.notifier.alert(this.get('networkError') + ` [${this.name}]`);
310
378
  return;
311
379
  }
312
380
  else {
313
381
  // Log
314
- console.error('API error', error);
382
+ console.error(`${this.name} API error`, error);
315
383
  }
316
384
  // Report the error
317
385
  this.notifier.alert(this.formatError(error));
@@ -326,7 +394,7 @@ class CoreApp {
326
394
  api.onRequest = (data) => {
327
395
  // Debug
328
396
  if (this.debug) {
329
- console.debug('CoreApp.setApiLoading.onRequest', api, data, this.notifier.loadingCount);
397
+ console.debug(`CoreApp.${this.name}.setApiLoading.onRequest`, api, data, this.notifier.loadingCount);
330
398
  }
331
399
  if (data.showLoading == null || data.showLoading) {
332
400
  this.notifier.showLoading();
@@ -336,13 +404,13 @@ class CoreApp {
336
404
  api.onComplete = (data) => {
337
405
  // Debug
338
406
  if (this.debug) {
339
- console.debug('CoreApp.setApiLoading.onComplete', api, data, this.notifier.loadingCount, this.lastCalled);
407
+ console.debug(`CoreApp.${this.name}.setApiLoading.onComplete`, api, data, this.notifier.loadingCount, this.lastCalled);
340
408
  }
341
409
  if (data.showLoading == null || data.showLoading) {
342
410
  this.notifier.hideLoading();
343
411
  // Debug
344
412
  if (this.debug) {
345
- console.debug('CoreApp.setApiLoading.onComplete.showLoading', api, this.notifier.loadingCount);
413
+ console.debug(`CoreApp.${this.name}.setApiLoading.onComplete.showLoading`, api, this.notifier.loadingCount);
346
414
  }
347
415
  }
348
416
  this.lastCalled = true;
@@ -553,11 +621,15 @@ class CoreApp {
553
621
  // Reset tryLogin state
554
622
  this._isTryingLogin = false;
555
623
  // Token countdown
556
- if (this.authorized)
557
- this.refreshCountdown(this.userData.seconds);
624
+ if (this.authorized) {
625
+ this.lastCalled = false;
626
+ if (refreshToken) {
627
+ this.updateApi(this.api.name, refreshToken, this.userData.seconds);
628
+ }
629
+ }
558
630
  else {
559
631
  this.cachedRefreshToken = undefined;
560
- this.refreshCountdownClear();
632
+ this.updateApi(this.api.name, undefined, -1);
561
633
  }
562
634
  // Host notice
563
635
  BridgeUtils_1.BridgeUtils.host?.userAuthorization(this.authorized);
@@ -1289,40 +1361,6 @@ class CoreApp {
1289
1361
  this.lastWarning?.dismiss();
1290
1362
  this.notifier.hideLoading(true);
1291
1363
  }
1292
- /**
1293
- * Refresh countdown
1294
- * @param seconds Seconds
1295
- */
1296
- refreshCountdown(seconds) {
1297
- // Make sure is big than 60 seconds
1298
- // Take action 60 seconds before expiry
1299
- seconds -= 60;
1300
- if (seconds <= 0)
1301
- return;
1302
- // Clear the current timeout seed
1303
- this.refreshCountdownClear();
1304
- // Reset last call flag
1305
- // Any success call will update it to true
1306
- // So first time after login will be always silent
1307
- this.lastCalled = false;
1308
- this.refreshCountdownSeed = window.setTimeout(() => {
1309
- if (this.lastCalled) {
1310
- // Call refreshToken to update access token
1311
- this.refreshToken();
1312
- }
1313
- else {
1314
- // Popup countdown for user action
1315
- this.freshCountdownUI();
1316
- }
1317
- }, 1000 * seconds);
1318
- }
1319
- refreshCountdownClear() {
1320
- // Clear the current timeout seed
1321
- if (this.refreshCountdownSeed > 0) {
1322
- window.clearTimeout(this.refreshCountdownSeed);
1323
- this.refreshCountdownSeed = 0;
1324
- }
1325
- }
1326
1364
  /**
1327
1365
  * Refresh token
1328
1366
  * @param props Props
@@ -1336,12 +1374,142 @@ class CoreApp {
1336
1374
  * Setup callback
1337
1375
  */
1338
1376
  setup() {
1377
+ // Done already
1378
+ if (this.isReady)
1379
+ return;
1339
1380
  // Ready
1340
1381
  this.isReady = true;
1341
1382
  // Restore
1342
1383
  this.restore();
1343
1384
  // Pending actions
1344
1385
  this.pendings.forEach((p) => p());
1386
+ // Setup scheduled tasks
1387
+ this.setupTasks();
1388
+ }
1389
+ /**
1390
+ * Exchange token data
1391
+ * @param api API
1392
+ * @param token Core system's refresh token to exchange
1393
+ * @returns Result
1394
+ */
1395
+ async exchangeToken(api, token) {
1396
+ // Call the API quietly, no loading bar and no error popup
1397
+ const data = await api.put('Auth/ExchangeToken', {
1398
+ token
1399
+ }, {
1400
+ showLoading: false,
1401
+ onError: (error) => {
1402
+ console.error(`CoreApp.${api.name}.ExchangeToken error`, error);
1403
+ // Prevent further processing
1404
+ return false;
1405
+ }
1406
+ });
1407
+ if (data) {
1408
+ // Update the access token
1409
+ api.authorize(data.tokenType, data.accessToken);
1410
+ // Update the API
1411
+ this.updateApi(api.name, data.refreshToken, data.expiresIn);
1412
+ // Update notice
1413
+ this.exchangeTokenUpdate(api, data);
1414
+ }
1415
+ }
1416
+ /**
1417
+ * Exchange token update, override to get the new token
1418
+ * @param api API
1419
+ * @param data API refresh token data
1420
+ */
1421
+ exchangeTokenUpdate(api, data) { }
1422
+ /**
1423
+ * Exchange intergration tokens for all APIs
1424
+ * @param token Core system's refresh token to exchange
1425
+ */
1426
+ exchangeTokenAll(token) {
1427
+ for (const name in this.apis) {
1428
+ const api = this.apis[name];
1429
+ this.exchangeToken(api[0], token);
1430
+ }
1431
+ }
1432
+ /**
1433
+ * API refresh token
1434
+ * @param api Current API
1435
+ * @param token Refresh token
1436
+ * @returns Result
1437
+ */
1438
+ async apiRefreshToken(api, token) {
1439
+ // Call the API quietly, no loading bar and no error popup
1440
+ const data = await api.put('Auth/ApiRefreshToken', {
1441
+ token
1442
+ }, {
1443
+ showLoading: false,
1444
+ onError: (error) => {
1445
+ console.error(`CoreApp.${api.name}.apiRefreshToken error`, error);
1446
+ // Prevent further processing
1447
+ return false;
1448
+ }
1449
+ });
1450
+ if (data == null)
1451
+ return undefined;
1452
+ // Update the access token
1453
+ api.authorize(data.tokenType, data.accessToken);
1454
+ // Return the new refresh token and access token expiration seconds
1455
+ return [data.refreshToken, data.expiresIn];
1456
+ }
1457
+ /**
1458
+ * Setup tasks
1459
+ */
1460
+ setupTasks() {
1461
+ shared_1.ExtendUtils.intervalFor(() => {
1462
+ // Exit when not authorized
1463
+ if (!this.authorized)
1464
+ return;
1465
+ // APIs
1466
+ for (const name in this.apis) {
1467
+ // Get the API
1468
+ const api = this.apis[name];
1469
+ // Skip the negative value or when refresh token is not set
1470
+ if (!api[4] || api[2] < 0)
1471
+ continue;
1472
+ // Minus one second
1473
+ api[2] -= 1;
1474
+ // Ready to trigger
1475
+ if (api[2] === 0) {
1476
+ // Refresh token
1477
+ api[3](api[0], api[4]).then((data) => {
1478
+ if (data == null) {
1479
+ // Failed, try it again in 2 seconds
1480
+ api[2] = 2;
1481
+ }
1482
+ else {
1483
+ // Reset the API
1484
+ const [token, seconds] = data;
1485
+ this.updateApi(api, token, seconds);
1486
+ }
1487
+ });
1488
+ }
1489
+ }
1490
+ for (let t = this.tasks.length - 1; t >= 0; t--) {
1491
+ // Get the task
1492
+ const task = this.tasks[t];
1493
+ // Minus one second
1494
+ task[2] -= 1;
1495
+ // Remove the tasks with negative value with splice
1496
+ if (task[2] < 0) {
1497
+ this.tasks.splice(t, 1);
1498
+ }
1499
+ else if (task[2] === 0) {
1500
+ // Ready to trigger
1501
+ // Reset the task
1502
+ task[2] = task[1];
1503
+ // Trigger the task
1504
+ task[0]().then((result) => {
1505
+ if (result === false) {
1506
+ // Asynchronous task, unsafe to splice the index, flag as pending
1507
+ task[2] = -1;
1508
+ }
1509
+ });
1510
+ }
1511
+ }
1512
+ }, 1000);
1345
1513
  }
1346
1514
  /**
1347
1515
  * Signout, with userLogout and toLoginPage
@@ -1366,8 +1534,9 @@ class CoreApp {
1366
1534
  * @param removeUrl Remove current URL for reuse
1367
1535
  */
1368
1536
  toLoginPage(tryLogin, removeUrl) {
1369
- const url = `/?tryLogin=${tryLogin ?? false}` +
1370
- (removeUrl ? '' : '&url=' + encodeURIComponent(location.href));
1537
+ // Save the current URL
1538
+ this.cachedUrl = removeUrl ? undefined : globalThis.location.href;
1539
+ const url = `/?tryLogin=${tryLogin ?? false}`;
1371
1540
  this.navigate(url);
1372
1541
  }
1373
1542
  /**
@@ -1,31 +1,32 @@
1
1
  /**
2
- * External settings items
2
+ * External endpoint
3
3
  */
4
- export interface IExternalSettings {
4
+ export type ExternalEndpoint = {
5
5
  /**
6
- * Core system API endpoint
6
+ * API endpoint
7
7
  */
8
8
  readonly endpoint: string;
9
+ /**
10
+ * Web url
11
+ */
12
+ readonly webUrl: string;
13
+ };
14
+ /**
15
+ * External settings items
16
+ */
17
+ export interface IExternalSettings extends ExternalEndpoint {
9
18
  /**
10
19
  * Message hub endpoint
11
20
  */
12
21
  readonly messageHub?: string;
13
22
  /**
14
- * Core system app root url
23
+ * App root url
15
24
  */
16
25
  readonly homepage: string;
17
26
  /**
18
- * Core system web url
19
- */
20
- readonly webUrl: string;
21
- /**
22
- * Service API endpoint
23
- */
24
- readonly serviceEndpoint?: string;
25
- /**
26
- * Service web Url
27
+ * Endpoints to other services
27
28
  */
28
- readonly serviceUrl?: string;
29
+ readonly endpoints?: Record<'core' | 'accounting' | 'crm' | 'calandar' | 'task' | string, ExternalEndpoint>;
29
30
  }
30
31
  /**
31
32
  * External settings namespace
@@ -35,6 +35,9 @@ var ExternalSettings;
35
35
  if (typeof value === 'string') {
36
36
  settings[key] = value.replace('{hostname}', hostname);
37
37
  }
38
+ else if (typeof value === 'object') {
39
+ format(value, hostname);
40
+ }
38
41
  }
39
42
  return settings;
40
43
  }