@mindline/sync 1.0.37 → 1.0.39

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.
@@ -2,5 +2,6 @@
2
2
  "ExpandedNodes": [
3
3
  ""
4
4
  ],
5
+ "SelectedNode": "\\index.ts",
5
6
  "PreviewInSolutionExplorer": false
6
7
  }
package/.vs/slnx.sqlite CHANGED
Binary file
Binary file
package/hybridspa.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  User,
4
4
  Tenant,
5
5
  Config,
6
+ TenantConfigInfo,
6
7
  APIResult
7
8
  } from "./index";
8
9
  import {
@@ -26,6 +27,7 @@ export const graphConfig = {
26
27
  "https://dev-configurationapi-westus.azurewebsites.net/api/v1/configurations",
27
28
  initEndpoint:
28
29
  "https://dev-configurationapi-westus.azurewebsites.net/api/v1/configuration/init",
30
+ readerStartSyncEndpoint: "https://dev-configurationapi-westus.azurewebsites.net/api/v1/startSync",
29
31
  tenantEndpoint:
30
32
  "https://dev-configurationapi-westus.azurewebsites.net/api/v1/tenant",
31
33
  tenantsEndpoint:
@@ -47,9 +49,6 @@ export const graphConfig = {
47
49
  authorityUSRegex: /^(https:\/\/login\.microsoftonline\.(?:us|com)\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
48
50
  authorityCN: "https://login.partner.microsoftonline.cn/",
49
51
  authorityCNRegex: /^(https:\/\/login\.partner\.microsoftonline\.cn\/)([\dA-Fa-f]{8}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{4}-[\dA-Fa-f]{12})\/oauth2\/authorize$/,
50
- // reader endpoint to trigger sync start
51
- readerStartSyncEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/startSync/",
52
- readerApiEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/lookup/"
53
52
  };
54
53
  // helper functions
55
54
  async function defineHeaders(
@@ -134,7 +133,7 @@ export async function adminDelete(
134
133
  console.log("Attempting DELETE from /admin: " + url!.href);
135
134
  let response = await fetch(url!.href, options);
136
135
  if (response.status === 200 && response.statusText === "OK") {
137
- console.log(`Successful DELETE from //admin: ${url!.href}`);
136
+ console.log(`Successful DELETE from /admin: ${url!.href}`);
138
137
  return result;
139
138
  } else {
140
139
  result.error = await processErrors(response);
@@ -335,13 +334,20 @@ export async function configPost(
335
334
  "isEnabled": ${config.isEnabled},
336
335
  "tenants": [`;
337
336
  config.tenants.map((tci) => {
337
+ // be sure we send null and not "null" in body
338
+ let sourceGroupId: string = tci.sourceGroupId != "" ? `"${tci.sourceGroupId}"` : "null";
339
+ let sourceGroupName: string = tci.sourceGroupName != "" ? `"${tci.sourceGroupName}"` : "null";
340
+ let targetGroupId: string = tci.targetGroupId != "" ? `"${tci.targetGroupId}"` : "null";
341
+ let targetGroupName: string = tci.targetGroupName != "" ? `"${tci.targetGroupName}"` : "null";
338
342
  // if last character is } we need a comma first
339
343
  let needComma: boolean = configBody.slice(-1) === "}";
340
344
  if (needComma) configBody += ",";
341
345
  configBody += `{
342
346
  "tenantId": "${tci.tid}",
343
- "sourceGroupId": "${tci.sourceGroupId}",
344
- "sourceGroupName": "${tci.sourceGroupName}",
347
+ "sourceGroupId": ${sourceGroupId},
348
+ "sourceGroupName": ${sourceGroupName},
349
+ "targetGroupId": ${targetGroupId},
350
+ "targetGroupName": ${targetGroupName},
345
351
  "configurationTenantType": "${tci.configurationTenantType}"
346
352
  }`;
347
353
  });
@@ -404,15 +410,21 @@ export async function configPut(
404
410
  "description": "${config.description}",
405
411
  "isEnabled": ${config.isEnabled},
406
412
  "tenants": [`;
407
- config.tenants.map((tci) => {
413
+ config.tenants.map((tci: TenantConfigInfo) => {
408
414
  // if last character is } we need a comma first
409
415
  let needComma: boolean = configBody.slice(-1) === "}";
410
416
  if (needComma) configBody += ",";
411
- // TODO: more sophisticated source tenant user filtering
417
+ // be sure we send null and not "null" in body
418
+ let sourceGroupId: string = tci.sourceGroupId != "" ? `"${tci.sourceGroupId}"` : "null";
419
+ let sourceGroupName: string = tci.sourceGroupName != "" ? `"${tci.sourceGroupName}"` : "null";
420
+ let targetGroupId: string = tci.targetGroupId != "" ? `"${tci.targetGroupId}"` : "null";
421
+ let targetGroupName: string = tci.targetGroupName != "" ? `"${tci.targetGroupName}"` : "null";
412
422
  configBody += `{
413
423
  "tenantId": "${tci.tid}",
414
- "sourceGroupId": "${tci.sourceGroupId}",
415
- "sourceGroupName": "${tci.sourceGroupName}",
424
+ "sourceGroupId": ${sourceGroupId},
425
+ "sourceGroupName": ${sourceGroupName},
426
+ "targetGroupId": ${targetGroupId},
427
+ "targetGroupName": ${targetGroupName},
416
428
  "configurationTenantType": "${tci.configurationTenantType}",
417
429
  "deltaToken": "${tci.deltaToken}"
418
430
  }`;
@@ -857,21 +869,21 @@ export async function readerPost(
857
869
  result.status = 500;
858
870
  return result;
859
871
  }
860
- // create reader endpoint with config ID
861
- let readerEndpoint: string = graphConfig.readerStartSyncEndpoint + config.id;
872
+ // create reader endpoint
873
+ let readerEndpoint: string = graphConfig.readerStartSyncEndpoint;
874
+ let url: URL = new URL(readerEndpoint);
875
+ url.searchParams.append("configurationId", config.id);
862
876
  // create headers
863
877
  const headers = await defineHeaders(instance, authorizedUser);
864
878
  // make reader endpoint call
865
879
  let options = { method: "POST", headers: headers };
866
880
  try {
867
- console.log("Attempting POST to /startSync: " + readerEndpoint);
868
- let response = await fetch(readerEndpoint, options);
881
+ console.log("Attempting POST to /startSync: " + url.href);
882
+ let response = await fetch(url.href, options);
869
883
  if (response.status === 200 && response.statusText === "OK") {
870
884
  console.log(`Successful POST to /startSync: ${readerEndpoint}`);
871
-
872
- let textResponse = await response.text();
873
- textResponse = textResponse;
874
-
885
+ let jsonResponse = await response.json();
886
+ result.array = JSON.parse(jsonResponse.PayloadStr);
875
887
  return result;
876
888
  } else {
877
889
  result.error = await processErrors(response);
package/index.d.ts CHANGED
@@ -65,11 +65,14 @@ declare module "@mindline/sync" {
65
65
  export class TenantConfigInfo {
66
66
  tid: string; // tenant identifier
67
67
  sourceGroupId: string; // source group - we can configure source group for reading
68
- sourceGroupName: string; // source group - we can configure source group for reading
68
+ sourceGroupName: string; //
69
+ targetGroupId: string; // target group - we can configure target group for writing
70
+ targetGroupName: string; //
69
71
  configurationTenantType: TenantConfigTypeStrings;
70
72
  deltaToken: string;
71
- filesWritten: number;
73
+ usersWritten: number;
72
74
  configId: string;
75
+ batchId: string;
73
76
  }
74
77
  export class Config {
75
78
  id: string;
@@ -134,12 +137,63 @@ declare module "@mindline/sync" {
134
137
  setEnd(endDate: Date): void;
135
138
  setStart(startDate: Date): void;
136
139
  }
140
+ export class Milestone {
141
+ Run: number;
142
+ Start: Date;
143
+ startDisplay: string;
144
+ POST: Date;
145
+ postDisplay: string;
146
+ Read: Date;
147
+ readDisplay: string;
148
+ Write: Date;
149
+ writeDisplay: string;
150
+ Duration: Date;
151
+ durationDisplay: string;
152
+ constructor(run: number);
153
+ start(start: string): void;
154
+ post(post: string): void;
155
+ read(read: string): void;
156
+ write(write: string): void;
157
+ }
158
+ export class MilestoneArray {
159
+ milestones: Milestone[];
160
+ constructor(bClearLocalStorage: boolean);
161
+ init(bClearLocalStorage: boolean): void;
162
+ save(): void;
163
+ start(setMilestones: (milestones: Milestone[]) => void): void;
164
+ unstart(setMilestones: (milestones: Milestone[]) => void): void;
165
+ post(setMilestones: (milestones: Milestone[]) => void): void;
166
+ read(setMilestones: (milestones: Milestone[]) => void): void;
167
+ write(setMilestones: (milestones: Milestone[]) => void): void;
168
+ #initFromObjects(milestones: Object[]): void;
169
+ }
137
170
  export class BatchArray {
138
171
  tenantNodes: TenantNode[];
172
+ pb_startTS: number;
173
+ pb_progress: number;
174
+ pb_increment: number;
175
+ pb_idle: number;
176
+ pb_idleMax: number;
177
+ pb_timer: NodeJS.Timer;
178
+ milestoneArray: MilestoneArray;
139
179
  constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
140
180
  // populate tenantNodes based on config tenants
141
- init(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
142
- startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined, setConfigSyncResult: (syncUpdate: string) => void): APIResult;
181
+ init(config: Config|null|undefined, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
182
+ initializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void;
183
+ uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void;
184
+ initializeSignalR(
185
+ config: Config | null | undefined,
186
+ syncPortalGlobalState: InitInfo | null,
187
+ batchIdArray: Array<Object>,
188
+ setRefreshDeltaTrigger: (trigger: boolean) => void,
189
+ setReadersTotal: (readersTotal: number) => void,
190
+ setReadersCurrent: (readersCurrent: number) => void,
191
+ setWritersTotal: (writersTotal: number) => void,
192
+ setWritersCurrent: (writersCurrent: number) => void,
193
+ setMilestones: (milestones: Milestone[]) => void,
194
+ setConfigSyncResult: (result: string) => void,
195
+ bClearLocalStorage: boolean): void;
196
+ startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): APIResult;
143
197
  }
144
198
  export class TenantNode {
145
199
  expanded: boolean;
@@ -159,6 +213,7 @@ declare module "@mindline/sync" {
159
213
  result: boolean;
160
214
  status: number;
161
215
  error: string;
216
+ array: Array<Object> | null;
162
217
  constructor();
163
218
  }
164
219
  //
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  //index.ts - published interface - AAD implementations, facade to Mindline Config API
2
2
  import * as signalR from "@microsoft/signalr"
3
3
  import { IPublicClientApplication, AuthenticationResult } from "@azure/msal-browser"
4
- import { deserializeArray } from 'class-transformer';
4
+ import { deserializeArray, instanceToPlain, ClassTransformOptions } from 'class-transformer';
5
5
  import { adminDelete, adminPost, adminsGet, configDelete, configsGet, configPost, configPut, graphConfig, initPost, readerPost, tenantPut, tenantPost, tenantDelete, tenantsGet, workspacesGet } from './hybridspa';
6
6
  import { version } from './package.json';
7
7
  import users from "./users.json";
@@ -9,6 +9,7 @@ import tenants from "./tenants.json";
9
9
  import configs from "./configs.json";
10
10
  import workspaces from "./workspaces.json";
11
11
  import tasksData from "./tasks";
12
+ import syncmilestones from './syncmilestones';
12
13
  import { log } from "console";
13
14
  const FILTER_FIELD = "workspaceIDs";
14
15
  // called by unit tests
@@ -112,18 +113,24 @@ export class TenantConfigInfo {
112
113
  tid: string;
113
114
  sourceGroupId: string;
114
115
  sourceGroupName: string;
116
+ targetGroupId: string;
117
+ targetGroupName: string;
115
118
  configurationTenantType: TenantConfigTypeStrings;
116
119
  deltaToken: string;
117
- filesWritten: number;
120
+ usersWritten: number;
118
121
  configId: string;
122
+ batchId: string;
119
123
  constructor() {
120
124
  this.tid = "";
121
125
  this.sourceGroupId = "";
122
126
  this.sourceGroupName = "";
127
+ this.targetGroupId = "";
128
+ this.targetGroupName = "";
123
129
  this.configurationTenantType = "source";
124
130
  this.deltaToken = "";
125
- this.filesWritten = 0;
131
+ this.usersWritten = 0;
126
132
  this.configId = "";
133
+ this.batchId = "";
127
134
  }
128
135
  }
129
136
  export class Config {
@@ -435,8 +442,196 @@ export class Task {
435
442
  };
436
443
  }
437
444
  // class corresponding to an execution of a Config - a *TenantNode* for each source tenant, each with a *TenantNode* array of target tenants
445
+ export class Milestone {
446
+ Run: number;
447
+ Start: Date;
448
+ startDisplay: string;
449
+ POST: Date;
450
+ postDisplay: string;
451
+ Read: Date;
452
+ readDisplay: string;
453
+ Write: Date;
454
+ writeDisplay: string;
455
+ Duration: Date;
456
+ durationDisplay: string;
457
+ constructor(run: number) {
458
+ this.Run = run;
459
+ this.start("");
460
+ this.POST = null;
461
+ this.postDisplay = "";
462
+ this.Read = null;
463
+ this.readDisplay = "";
464
+ this.Write = null;
465
+ this.writeDisplay = "";
466
+ this.Duration = null;
467
+ this.durationDisplay = "";
468
+ }
469
+ start(start: string): void {
470
+ start == "" ? this.Start = new Date() : this.Start = new Date(start);
471
+ this.startDisplay = `${this.Start.getMinutes().toString().padStart(2, "0")}:${this.Start.getSeconds().toString().padStart(2, "0")}`;
472
+ }
473
+ post(post: string): void {
474
+ post == "" ? this.POST = new Date() : this.POST = new Date(post);
475
+ this.postDisplay = `${this.POST.getMinutes().toString().padStart(2, "0")}:${this.POST.getSeconds().toString().padStart(2, "0")}`;
476
+ }
477
+ read(read: string): void {
478
+ read == "" ? this.Read = new Date() : this.Read = new Date(read);
479
+ this.readDisplay = `${this.Read.getMinutes().toString().padStart(2, "0")}:${this.Read.getSeconds().toString().padStart(2, "0")}`;
480
+ }
481
+ write(write: string): void {
482
+ write == "" ? this.Write = new Date() : this.Write = new Date(write);
483
+ this.writeDisplay = `${this.Write.getMinutes().toString().padStart(2, "0")}:${this.Write.getSeconds().toString().padStart(2, "0")}`;
484
+ this.Duration = new Date(this.Write.getTime() - this.Start.getTime());
485
+ this.durationDisplay = `${this.Duration.getMinutes().toString().padStart(2, "0")}:${this.Duration.getSeconds().toString().padStart(2, "0")}`;
486
+ }
487
+ }
488
+ export class MilestoneArray {
489
+ milestones: Milestone[];
490
+ constructor(bClearLocalStorage: boolean) {
491
+ this.init(bClearLocalStorage);
492
+ }
493
+ init(bClearLocalStorage: boolean): void {
494
+ // read from localstorage by default
495
+ if (storageAvailable("localStorage")) {
496
+ let result = localStorage.getItem("syncmilestones");
497
+ if (result != null && typeof result === "string" && result !== "") {
498
+ let milestonesString: string = result;
499
+ let milestones: Object [] = JSON.parse(milestonesString);
500
+ if (milestones.length !== 0) {
501
+ if (bClearLocalStorage) {
502
+ localStorage.removeItem("syncmilestones");
503
+ }
504
+ else {
505
+ this.#initFromObjects(milestones);
506
+ return;
507
+ }
508
+ }
509
+ }
510
+ }
511
+ // if storage unavailable or we were just asked to clear, read from default syncmilestone file
512
+ this.#initFromObjects(syncmilestones);
513
+ }
514
+ save(): void {
515
+ let milestonesString: string = JSON.stringify(this.milestones);
516
+ if (storageAvailable("localStorage")) {
517
+ localStorage.setItem("syncmilestones", milestonesString);
518
+ }
519
+ }
520
+ // milestone tracking during a sync
521
+ start(setMilestones: (milestones: Milestone[]) => void): void {
522
+ // we should always have a milestone array and a first milestone
523
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
524
+ let currentRun: number = Number(this.milestones[0].Run);
525
+ // create a new milestone and prepend to front of array
526
+ let newMilestone: Milestone = new Milestone(currentRun+1);
527
+ this.milestones.unshift(newMilestone);
528
+ // re-define milestone array to trigger render
529
+ this.milestones = this.milestones.map((ms: Milestone) => {
530
+ let newms = new Milestone(ms.Run);
531
+ newms.Start = ms.Start;
532
+ newms.startDisplay = ms.startDisplay;
533
+ newms.POST = ms.POST;
534
+ newms.postDisplay = ms.postDisplay;
535
+ newms.Read = ms.Read;
536
+ newms.readDisplay = ms.readDisplay;
537
+ newms.Write = ms.Write;
538
+ newms.writeDisplay = ms.writeDisplay;
539
+ newms.Duration = ms.Duration;
540
+ newms.durationDisplay = ms.durationDisplay;
541
+ return newms;
542
+ });
543
+ setMilestones(this.milestones);
544
+ console.log(`Start milestone: ${this.milestones[0].Run}:${this.milestones[0].Start}`);
545
+ }
546
+ unstart(setMilestones: (milestones: Milestone[]) => void): void {
547
+ // we should always have a milestone array and a first milestone
548
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
549
+ let currentRun: number = Number(this.milestones[0].Run);
550
+ // remove first milestone from front of array
551
+ let removedMilestone: Milestone = this.milestones.shift();
552
+ // re-define milestone array to trigger render
553
+ this.milestones = this.milestones.map((ms: Milestone) => {
554
+ let newms = new Milestone(ms.Run);
555
+ newms.Start = ms.Start;
556
+ newms.startDisplay = ms.startDisplay;
557
+ newms.POST = ms.POST;
558
+ newms.postDisplay = ms.postDisplay;
559
+ newms.Read = ms.Read;
560
+ newms.readDisplay = ms.readDisplay;
561
+ newms.Write = ms.Write;
562
+ newms.writeDisplay = ms.writeDisplay;
563
+ newms.Duration = ms.Duration;
564
+ newms.durationDisplay = ms.durationDisplay;
565
+ return newms;
566
+ });
567
+ setMilestones(this.milestones);
568
+ console.log(`Unstart removed first milestone: ${removedMilestone.Run}:${removedMilestone.Start}`);
569
+ }
570
+ post(setMilestones: (milestones: Milestone[]) => void): void {
571
+ // update the post value of the first milestone
572
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
573
+ this.milestones[0].post("");
574
+ setMilestones(this.milestones);
575
+ console.log(`POST milestone: ${this.milestones[0].Run}:${this.milestones[0].POST}`);
576
+ }
577
+ read(setMilestones: (milestones: Milestone[]) => void): void {
578
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
579
+ this.milestones[0].read("");
580
+ setMilestones(this.milestones);
581
+ console.log(`Read milestone: ${this.milestones[0].Run}:${this.milestones[0].Read}`);
582
+ }
583
+ write(setMilestones: (milestones: Milestone[]) => void): void {
584
+ if (this.milestones == null || this.milestones.length < 1) { debugger; return; }
585
+ this.milestones[0].write("");
586
+ // while we have >10 complete milestones, remove the last
587
+ while (this.milestones.length > 10) {
588
+ let removed: Milestone = this.milestones.pop();
589
+ console.log(`Removed milestone #${removed.Run}: ${removed.Start}`);
590
+ }
591
+ // save to localstorage
592
+ this.save();
593
+ // re-define milestone array to trigger render
594
+ this.milestones = this.milestones.map((ms: Milestone) => {
595
+ let newms = new Milestone(ms.Run);
596
+ newms.Start = ms.Start;
597
+ newms.startDisplay = ms.startDisplay;
598
+ newms.POST = ms.POST;
599
+ newms.postDisplay = ms.postDisplay;
600
+ newms.Read = ms.Read;
601
+ newms.readDisplay = ms.readDisplay;
602
+ newms.Write = ms.Write;
603
+ newms.writeDisplay = ms.writeDisplay;
604
+ newms.Duration = ms.Duration;
605
+ newms.durationDisplay = ms.durationDisplay;
606
+ return newms;
607
+ });
608
+ setMilestones(this.milestones);
609
+ }
610
+ #initFromObjects(milestones: Object[]): void {
611
+ if (milestones == null) {
612
+ this.milestones = new Array();
613
+ }
614
+ else {
615
+ this.milestones = milestones.map((milestone: Object) => {
616
+ let ms: Milestone = new Milestone(Number(milestone.Run));
617
+ ms.start(milestone.Start);
618
+ ms.post(milestone.POST);
619
+ ms.read(milestone.Read);
620
+ ms.write(milestone.Write);
621
+ return ms;
622
+ });
623
+ }
624
+ }
625
+ }
438
626
  export class BatchArray {
439
627
  tenantNodes: TenantNode[];
628
+ pb_startTS: number;
629
+ pb_progress: number;
630
+ pb_increment: number;
631
+ pb_idle: number;
632
+ pb_idleMax: number;
633
+ pb_timer: NodeJS.Timer;
634
+ milestoneArray: MilestoneArray;
440
635
  constructor(
441
636
  config: Config | null,
442
637
  syncPortalGlobalState: InitInfo | null,
@@ -444,30 +639,36 @@ export class BatchArray {
444
639
  ) {
445
640
  this.tenantNodes = new Array<TenantNode>();
446
641
  this.init(config, syncPortalGlobalState, bClearLocalStorage);
642
+ this.pb_startTS = 0;
643
+ this.pb_progress = 0;
644
+ this.pb_increment = 0;
645
+ this.pb_timer = null;
646
+ this.pb_idle = 0;
647
+ this.pb_idleMax = 0;
648
+ this.milestoneArray = new MilestoneArray(false);
447
649
  }
448
650
  // populate tenantNodes based on config tenants
449
651
  init(
450
- config: Config | null,
652
+ config: Config | null | undefined,
451
653
  syncPortalGlobalState: InitInfo | null,
452
654
  bClearLocalStorage: boolean
453
655
  ): void {
454
656
  console.log(
455
- `Calling BatchArray::init(config: "${config ? config.name : "null"
456
- }", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
657
+ `Calling BatchArray::init(config: "${config ? config.name : "null"}", bClearLocalStorage: ${bClearLocalStorage ? "true" : "false"})`
457
658
  );
458
- // first clear batch array
459
- this.tenantNodes.length = 0;
460
- // then clear localStorage if we have been asked to
659
+ // clear localStorage if we have been asked to
461
660
  if (bClearLocalStorage) {
462
- if (storageAvailable("localStorage"))
661
+ if (storageAvailable("localStorage")) {
463
662
  localStorage.removeItem(config.name);
663
+ this.milestoneArray.init(bClearLocalStorage);
664
+ }
464
665
  }
465
666
  // create BatchArray if passed Config and InitInfo
466
- if (
467
- config != null &&
667
+ if (config != null &&
468
668
  config.tenants != null &&
469
- syncPortalGlobalState != null
470
- ) {
669
+ syncPortalGlobalState != null) {
670
+ // clear batch array only if we have been passed something with which to replace it
671
+ this.tenantNodes.length = 0;
471
672
  // create a sourceTenantNode for each Source and SourceTarget
472
673
  config.tenants.map((tciPotentialSource: TenantConfigInfo) => {
473
674
  if (
@@ -480,7 +681,8 @@ export class BatchArray {
480
681
  if (sourceTenant != null) {
481
682
  let sourceTenantNode: TenantNode = new TenantNode(
482
683
  tciPotentialSource.tid,
483
- sourceTenant.name
684
+ sourceTenant.name,
685
+ tciPotentialSource.batchId
484
686
  );
485
687
  this.tenantNodes.push(sourceTenantNode);
486
688
  } else {
@@ -508,7 +710,8 @@ export class BatchArray {
508
710
  if (targetTenant != null) {
509
711
  let targetTenantNode: TenantNode = new TenantNode(
510
712
  tciPotentialTarget.tid,
511
- targetTenant.name
713
+ targetTenant.name,
714
+ tciPotentialTarget.batchId
512
715
  );
513
716
  sourceTenantNode.targets.push(targetTenantNode);
514
717
  sourceTenantNode.expanded = true;
@@ -523,195 +726,255 @@ export class BatchArray {
523
726
  }
524
727
  });
525
728
  });
526
- // then try localStorage to find any matching source tenant metrics
527
- if (storageAvailable("localStorage")) {
528
- let result = localStorage.getItem(config.name);
529
- if (result != null && typeof result === "string" && result !== "") {
530
- // TODO: retrieve any relevant stored statistics from localStorage
531
- // let batchArrayString: string = result;
532
- // let batchArray: BatchArray = JSON.parse(batchArrayString);
533
- // batchArray.batches.map((batch: Batch) => {
534
- // config.tenants.map((tciTarget: TenantConfigInfo) => {
535
- // if(tciTarget.tid !== batch.tid) {
536
- // let target: Target = new Target(tciTarget.tid);
537
- // batch.targets.push(target);
538
- // }
539
- // });
540
- // });
729
+ }
730
+ }
731
+ initializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
732
+ this.pb_startTS = Date.now();
733
+ this.pb_progress = 0;
734
+ this.pb_increment = 1;
735
+ this.pb_idle = 0;
736
+ this.pb_idleMax = 0;
737
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
738
+ this.pb_timer = setInterval(() => {
739
+ // if we go 20 seconds without a signalR message, finish the sync
740
+ this.pb_idle = this.pb_idle + 1;
741
+ this.pb_idleMax = Math.max(this.pb_idle, this.pb_idleMax);
742
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
743
+ if (this.pb_idle >= 20) {
744
+ clearInterval(this.pb_timer);
745
+ this.pb_timer = null;
746
+ if (this.milestoneArray.milestones[0].Write == null) {
747
+ this.milestoneArray.write(setMilestones);
541
748
  }
749
+ setConfigSyncResult(`finished sync, no updates for ${this.pb_idle} seconds`);
542
750
  }
543
- }
751
+ // if we get to 100, stop the timer, let SignalR or countdown timer finish sync
752
+ if (this.pb_progress < 100) {
753
+ this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
754
+ setSyncProgress(this.pb_progress);
755
+ }
756
+ }, 1000);
757
+ this.milestoneArray.start(setMilestones);
544
758
  }
545
- // start a sync cycle
546
- async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined, setConfigSyncResult: (syncUpdate: string) => void): Promise<APIResult> {
547
- let result: APIResult = new APIResult();
548
- if (this.tenantNodes == null || this.tenantNodes.length == 0) {
549
- // we should not have an empty batch array for a test
550
- debugger;
551
- result.result = false;
552
- result.error = "startSync: invalid parameters";
553
- result.status = 500;
554
- return result;
555
- }
759
+ uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
760
+ this.pb_startTS = 0;
761
+ this.pb_progress = 0;
762
+ setSyncProgress(this.pb_progress);
763
+ setConfigSyncResult("sync failed to execute");
764
+ this.pb_increment = 0;
765
+ clearInterval(this.pb_timer);
766
+ this.pb_timer = null;
767
+ this.pb_idle = 0;
768
+ this.pb_idleMax = 0;
769
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
770
+ this.milestoneArray.unstart(setMilestones);
771
+ }
772
+ initializeSignalR(
773
+ config: Config | null | undefined,
774
+ syncPortalGlobalState: InitInfo | null,
775
+ batchIdArray: Array<Object>,
776
+ setRefreshDeltaTrigger: (trigger: boolean) => void,
777
+ setReadersTotal: (readersTotal: number) => void,
778
+ setReadersCurrent: (readersCurrent: number) => void,
779
+ setWritersTotal: (writersTotal: number) => void,
780
+ setWritersCurrent: (writersCurrent: number) => void ,
781
+ setMilestones: (milestones: Milestone[]) => void,
782
+ setConfigSyncResult: (result: string) => void,
783
+ bClearLocalStorage: boolean
784
+ ): void {
785
+ // we have just completed a successful POST to startSync
786
+ this.milestoneArray.post(setMilestones);
787
+ setConfigSyncResult("started sync, waiting for updates...");
788
+ // re-initialize batch array with Configuration updated by the succcessful POST to startSync
789
+ this.init(config, syncPortalGlobalState, false);
556
790
  // define newMessage handler that can access *this*
557
791
  let handler = (message) => {
558
792
  console.log(message);
559
793
  let item = JSON.parse(message);
794
+ // reset the countdown timer every time we get a message
795
+ this.pb_idle = 0;
560
796
  // find the associated tenant for this SignalR message
561
- let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === item.TargetID);
797
+ let matchingPair: Object = batchIdArray.find((o: Object) => o.BatchId == item.TargetID);
798
+ if (matchingPair == null) {
799
+ console.log(`Batch ${item.TargetID} not found in batchIdArray.`);
800
+ debugger;
801
+ return;
802
+ }
803
+ let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === matchingPair.SourceId);
562
804
  if (tenantNode == null) { // null OR undefined
563
- console.log(`${item.TargetID} not found in BatchArray.`);
805
+ console.log(`Tenant ${matchingPair.SourceId} not found in BatchArray.`);
564
806
  debugger;
565
807
  return;
566
808
  }
567
- let writerNode: TenantNode|null = null;
568
- // process stats for this SignalR message
809
+ tenantNode.batchId = matchingPair.BatchId;
810
+ // process stats for this SignalR message batch
569
811
  let statsarray = item.Stats; // get the array of statistics
570
812
  let statskeys = Object.keys(statsarray); // get the keys of the array
571
813
  let statsvalues = Object.values(statsarray); // get the values of the array
572
814
  for (let j = 0; j < statskeys.length; j++) {
815
+ let bTotalCount = statskeys[j].endsWith("TotalCount");
816
+ let bCurrentCount = statskeys[j].endsWith("CurrentCount");
817
+ let bDeferredCount = statskeys[j].endsWith("DeferredCount");
573
818
  if (statskeys[j].startsWith("Reader")) {
574
- if (statskeys[j].endsWith("TotalCount")) {
575
- // parse batchId from key and store in TenantNode for this batch
576
- let batchidRegexp = /Reader\/BID:(.+)\/TotalCount/;
577
- let matchBID = statskeys[j].match(batchidRegexp);
578
- if (matchBID == null) {
579
- console.log(`batchId not found in ${statskeys[j]}.`);
580
- debugger;
581
- return;
582
- }
583
- // integrate any queued Writers that have been waiting for this batchId to be assigned to a Reader
584
- let idx: number = this.tenantNodes.findIndex((t: TenantNode) => (t.name === "QUEUED WRITER" && t.batchId === matchBID[1]));
585
- while (idx !== -1) {
586
- let queuedWriter: TenantNode = this.tenantNodes.splice(idx, 1)[0];
587
- let actualWriter: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid == queuedWriter.tid);
588
- if (actualWriter == null) {
589
- console.log(`could not find queued writer ${queuedWriter.tid}.`);
590
- debugger;
591
- }
592
- else {
593
- actualWriter.update(queuedWriter.total, queuedWriter.read, queuedWriter.written, queuedWriter.deferred);
594
- }
595
- idx = this.tenantNodes.findIndex((t: TenantNode) => (t.name === "QUEUED WRITER" && t.batchId === matchBID[1]));
596
- }
597
- // update Read node
598
- tenantNode.batchId = matchBID[1];
599
- tenantNode.total = Number(statsvalues[j]);
600
- console.log(`----- ${tenantNode.name} batchId: ${tenantNode.batchId}`);
601
- console.log(`----- ${tenantNode.name} Total Read: ${tenantNode.total}`);
819
+ // parse tid from Reader key
820
+ let tidRegexp = /Reader\/TID:(.+)\/TotalCount/;
821
+ if (bCurrentCount) tidRegexp = /Reader\/TID:(.+)\/CurrentCount/;
822
+ if (bDeferredCount) tidRegexp = /Reader\/TID:(.+)\/DeferredCount/;
823
+ let matchTID = statskeys[j].match(tidRegexp);
824
+ if (matchTID == null) {
825
+ console.log(`tid not found in ${statskeys[j]}.`);
826
+ debugger;
827
+ return;
828
+ }
829
+ if (bTotalCount) {
830
+ tenantNode.total = Math.max(Number(statsvalues[j]), tenantNode.total);
831
+ console.log(`----- ${tenantNode.name} TID: ${tenantNode.tid} batchId: ${tenantNode.batchId}`);
832
+ console.log(`----- ${tenantNode.name} Total To Read: ${tenantNode.total}`);
602
833
  }
603
- if (statskeys[j].endsWith("CurrentCount")) {
604
- tenantNode.read = Number(statsvalues[j]);
834
+ else {
835
+ tenantNode.read = Math.max(Number(statsvalues[j]), tenantNode.read);
605
836
  console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
606
837
  }
607
838
  }
608
839
  if (statskeys[j].startsWith("Writer")) {
609
- if (statskeys[j].endsWith("TotalCount")) {
610
- // parse batchId from Writer key
611
- let batchidRegexp = /Writer\/BID:(.+)\/TotalCount/;
612
- let matchBID = statskeys[j].match(batchidRegexp);
613
- if (matchBID == null) {
614
- console.log(`batchId not found in ${statskeys[j]}.`);
615
- debugger;
616
- return;
617
- }
618
- // find the Writer node for this tenant under the Reader node of a different tenant
619
- let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
620
- if (readerNode == null) {
621
- // reader for this batch has not yet been encountered, queue this writer for processing when reader arrives
622
- writerNode = new TenantNode(tenantNode.tid, "QUEUED WRITER");
623
- writerNode.batchId = matchBID[1];
624
- writerNode.total = Number(statsvalues[j]);
625
- this.tenantNodes.push(writerNode);
626
- console.log(`----- QUEUED batchId: ${writerNode.batchId}`);
627
- console.log(`----- QUEUED Total To Write: ${writerNode.total}`);
628
- }
629
- else {
630
- if (readerNode.name == "QUEUED WRITER") {
631
- // update queued node
632
- writerNode = readerNode;
633
- }
634
- else {
635
- // reader is legit, find writer node under this reader node
636
- writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
637
- if (writerNode == null) {
638
- console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
639
- debugger;
640
- return;
641
- }
642
- }
643
- writerNode.batchId = matchBID[1];
644
- writerNode.total = Number(statsvalues[j]);
645
- console.log(`----- ${writerNode.name} batchId: ${writerNode.batchId}`);
646
- console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
647
- }
840
+ // parse tid from Writer key
841
+ let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
842
+ if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
843
+ if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
844
+ let matchTID = statskeys[j].match(tidRegexp);
845
+ if (matchTID == null) {
846
+ console.log(`tid not found in ${statskeys[j]}.`);
847
+ debugger;
848
+ return;
648
849
  }
649
- if (statskeys[j].endsWith("CurrentCount")) {
650
- // parse batchId from Writer key
651
- let batchidRegexp = /Writer\/BID:(.+)\/CurrentCount/;
652
- let matchBID = statskeys[j].match(batchidRegexp);
653
- if (matchBID == null) {
654
- console.log(`batchId not found in ${statskeys[j]}.`);
655
- debugger;
656
- return;
657
- }
658
- // find the Writer node for this tenant under the Reader node of a different tenant
659
- let readerNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.batchId === matchBID[1]);
660
- if (readerNode == null) {
661
- // no reader or queued writer for this batch
662
- console.log(`No reader or queued writer for batch ${matchBID[1]}.`);
663
- debugger;
664
- return;
665
- }
666
- if (readerNode.name == "QUEUED WRITER") {
667
- // update queued node
668
- writerNode = readerNode;
669
- }
670
- else {
671
- // reader is legit, find writer node under this reader node
672
- writerNode = readerNode.targets.find((t: TenantNode) => t.tid === tenantNode.tid);
673
- if (writerNode == null) {
674
- console.log(`Writer ${tenantNode.name} not found under Reader ${readerNode.name}.`);
675
- debugger;
676
- return;
677
- }
678
- }
679
- writerNode.batchId = matchBID[1];
680
- writerNode.written = Number(statsvalues[j]);
681
- console.log(`${writerNode.name} batchId: ${writerNode.batchId}`);
682
- console.log(`${writerNode.name} Total Written: ${writerNode.written}`);
850
+ // this Writer node should exist precisely under the Reader for this SignalR message
851
+ let writerNode: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid === matchTID[1]);
852
+ if (writerNode == null) {
853
+ console.log(`Writer ${tenantNode.name} not found under Reader ${tenantNode.name}.`);
854
+ debugger;
855
+ return;
683
856
  }
857
+ writerNode.batchId = matchingPair.BatchId;
858
+ if (bTotalCount) {
859
+ writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
860
+ console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
861
+ console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
862
+ }
863
+ else if (bCurrentCount) {
864
+ writerNode.written = Math.max(Number(statsvalues[j]), writerNode.written);
865
+ console.log(`----- ${writerNode.name} Total Written: ${writerNode.written}`);
866
+ }
867
+ else if (bDeferredCount) {
868
+ writerNode.deferred = Math.max(Number(statsvalues[j]), writerNode.deferred);
869
+ console.log(`----- ${writerNode.name} Total Deferred: ${writerNode.deferred}`);
870
+ }
871
+ else {
872
+ console.log(`unknown writer type`);
873
+ debugger;
874
+ return;
875
+ }
876
+ writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
684
877
  }
685
878
  }
879
+ // update status based on all updates in this message
686
880
  tenantNode.update(tenantNode.total, tenantNode.read, tenantNode.written, tenantNode.deferred);
687
- if (writerNode != null) {
688
- writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
689
- if (tenantNode.total === tenantNode.read && writerNode.total === writerNode.written) {
690
- setConfigSyncResult("complete");
691
- console.log(`Setting config sync result: "complete"`);
692
- }
693
- else {
694
- setConfigSyncResult("writing in progress");
695
- console.log(`Setting config sync result: "writing in progress"`);
696
- }
881
+ // for each message, enumerate nodes to assess completion state
882
+ let bReadingComplete: boolean = true;
883
+ let bWritingComplete: boolean = true;
884
+ let bWritingStarted: boolean = false;
885
+ let readerTotal: number = 0;
886
+ let readerCurrent: number = 0;
887
+ let writerTotal: number = 0;
888
+ let writerCurrent: number = 0;
889
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
890
+ sourceTenantNode.targets.map((writerNode: TenantNode) => {
891
+ bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
892
+ bWritingStarted ||= (writerNode.total > 0 || writerNode.status != "not started");
893
+ writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
894
+ writerCurrent += writerNode.written;
895
+ });
896
+ bReadingComplete &&= (sourceTenantNode.status == "complete" || sourceTenantNode.status == "failed");
897
+ readerTotal += sourceTenantNode.total;
898
+ readerCurrent += sourceTenantNode.read;
899
+ });
900
+ // set linear gauge max and current values
901
+ setReadersTotal(readerTotal);
902
+ setReadersCurrent(readerCurrent);
903
+ setWritersTotal(Math.max(writerTotal, readerTotal));
904
+ setWritersCurrent(writerCurrent);
905
+ // because it is an important milestone, we always check if we have *just* completed reading
906
+ if (bReadingComplete && this.milestoneArray.milestones[0].Read == null) {
907
+ this.milestoneArray.read(setMilestones);
908
+ setConfigSyncResult("reading complete");
909
+ console.log(`Setting config sync result: "reading complete"`);
910
+ // trigger refresh delta tokens
911
+ setRefreshDeltaTrigger(true);
912
+ // change to % per second to complete in 7x as long as it took to get here
913
+ let readTS = Date.now();
914
+ let secsElapsed = (readTS - this.pb_startTS) / 1000;
915
+ let expectedPercentDone = 7;
916
+ let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
917
+ this.pb_increment = expectedPercentPerSecond;
918
+ console.log(`Setting increment: ${this.pb_increment}% per second`);
697
919
  }
698
- else {
920
+ // with that out of the way, is writing complete?
921
+ if (bWritingComplete) {
922
+ this.milestoneArray.write(setMilestones);
923
+ setConfigSyncResult("sync complete");
924
+ console.log(`Setting config sync result: "complete"`);
925
+ this.pb_progress = 99;
926
+ }
927
+ // if not, has writing even started?
928
+ else if (bWritingStarted) {
929
+ setConfigSyncResult("writing in progress");
930
+ console.log(`Setting config sync result: "writing in progress"`);
931
+ }
932
+ // else, we must be reading (unless we already completed reading)
933
+ else if (this.milestoneArray.milestones[0].Read == null){
699
934
  setConfigSyncResult("reading in progress");
700
935
  console.log(`Setting config sync result: "reading in progress"`);
701
936
  }
702
937
  }
703
- // start SignalR connection for each source tenant in the configuration
704
- this.tenantNodes.map((sourceTenantNode: TenantNode) => {
705
- const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${sourceTenantNode.tid}`;
938
+ // start SignalR connection based on each batchId
939
+ batchIdArray.map((batchPair: Object) => {
940
+ const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${batchPair.BatchId}`;
941
+ console.log(`Creating SignalR Hub for TID: ${batchPair.SourceId} ${endpointUrl}`);
706
942
  const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
707
943
  .withUrl(endpointUrl)
708
- // .withAutomaticReconnect() TODO: full auto-reconnect implementation
944
+ .withAutomaticReconnect()
709
945
  .configureLogging(signalR.LogLevel.Information)
710
946
  .build();
711
947
  // when you get a message, process the message
712
948
  connection.on("newMessage", handler);
949
+ connection.onreconnecting(error => {
950
+ console.assert(connection.state === signalR.HubConnectionState.Reconnecting);
951
+ console.log(`Connection lost due to error "${error}". Reconnecting.`);
952
+ });
953
+ connection.onreconnected(connectionId => {
954
+ console.assert(connection.state === signalR.HubConnectionState.Connected);
955
+ console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
956
+ });
957
+ // restart when you get a close event
958
+ connection.onclose(async () => {
959
+ console.log(`Connection closing. Attempting restart.`);
960
+ await connection.start();
961
+ });
962
+ // start and display any caught exceptions in the console
713
963
  connection.start().catch(console.error);
714
964
  });
965
+ }
966
+ // start a sync cycle
967
+ async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): Promise<APIResult>
968
+ {
969
+ let result: APIResult = new APIResult();
970
+ if (this.tenantNodes == null || this.tenantNodes.length == 0) {
971
+ // we should not have an empty batch array for a test
972
+ debugger;
973
+ result.result = false;
974
+ result.error = "startSync: invalid parameters";
975
+ result.status = 500;
976
+ return result;
977
+ }
715
978
  // execute post to reader endpoint
716
979
  result = await readerPost(instance, authorizedUser, config);
717
980
  return result;
@@ -728,11 +991,11 @@ export class TenantNode {
728
991
  written: number;
729
992
  deferred: number;
730
993
  targets: TenantNode[];
731
- constructor(tid: string, name: string) {
994
+ constructor(tid: string, name: string, batchId: string) {
732
995
  this.expanded = false;
733
996
  this.name = name;
734
997
  this.tid = tid;
735
- this.batchId = "";
998
+ this.batchId = batchId;
736
999
  this.targets = new Array<TenantNode>();
737
1000
  this.update(0, 0, 0, 0);
738
1001
  }
@@ -749,7 +1012,7 @@ export class TenantNode {
749
1012
  else if (this.written > 0) {
750
1013
  if (this.written + this.deferred < this.total) this.status = "in progress";
751
1014
  else if (this.written === this.total) this.status = "complete";
752
- else if (this.written + this.deferred === this.total) this.status = "failed";
1015
+ else if (this.written + this.deferred >= this.total) this.status = "failed";
753
1016
  }
754
1017
  }
755
1018
  }
@@ -826,7 +1089,7 @@ export function signIn(user: User, tasks: TaskArray): void {
826
1089
  tenantURL += "MicrosoftIdentity/Account/Challenge";
827
1090
  let url: URL = new URL(tenantURL);
828
1091
  url.searchParams.append("redirectUri", window.location.origin);
829
- url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
1092
+ url.searchParams.append("scope", "openid offline_access Directory.AccessAsUser.All CrossTenantInformation.ReadBasic.All");
830
1093
  url.searchParams.append("domainHint", "organizations");
831
1094
  if (user.oid !== "1") {
832
1095
  url.searchParams.append("loginHint", user.mail);
@@ -1173,7 +1436,7 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1173
1436
  returnedAdmins.map((item) => {
1174
1437
  // are we already tracking this user?
1175
1438
  let user: User | null = null;
1176
- let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
1439
+ let usIndex = ii.us.findIndex((u) => (u.oid === item.userId || u.oid === item.email));
1177
1440
  if (usIndex === -1) {
1178
1441
  // start tracking
1179
1442
  let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
@@ -1192,13 +1455,13 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1192
1455
  user = ii.us.at(usIndex);
1193
1456
  }
1194
1457
  // refresh all the data available from the server
1195
- user.oid = item.userId;
1458
+ user.oid = item.userId ? item.userId : item.email;
1196
1459
  user.name = item.firstName;
1197
1460
  user.mail = item.email;
1198
1461
  user.tid = item.tenantId;
1199
1462
  // ensure this workspace tracks this user
1200
- let idx = workspace.associatedUsers.findIndex((u) => u === item.userId);
1201
- if (idx == -1) workspace.associatedUsers.push(item.userId);
1463
+ let idx = workspace.associatedUsers.findIndex((u) => u === user.oid);
1464
+ if (idx == -1) workspace.associatedUsers.push(user.oid);
1202
1465
  });
1203
1466
  }
1204
1467
  function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>) {
@@ -1227,7 +1490,12 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1227
1490
  tenant.tenantType = item.type.toLowerCase(); // should now be strings
1228
1491
  tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
1229
1492
  tenant.onboarded = item.isOnboarded ? "true" : "false";
1230
- tenant.authority = item.authority;
1493
+
1494
+ // canonicalize authority when getting it from config backend
1495
+ const regex = /^(https:\/\/login.microsoftonline.(?:us|com)\/)organizations\/v2.0$/;
1496
+ const regexMatch = item.authority.match(regex);
1497
+ tenant.authority = regexMatch ? regexMatch[1] : item.authority;
1498
+
1231
1499
  tenant.readServicePrincipal = item.readServicePrincipal;
1232
1500
  tenant.writeServicePrincipal = item.writeServicePrincipal;
1233
1501
  // ensure this workspace tracks this tenant
@@ -1265,11 +1533,14 @@ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConf
1265
1533
  item.tenants.map((tci) => {
1266
1534
  let tenantConfigInfo = new TenantConfigInfo();
1267
1535
  tenantConfigInfo.tid = tci.tenantId;
1268
- tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
1269
- tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1536
+ tenantConfigInfo.sourceGroupId = tci.sourceGroupId ?? "";
1537
+ tenantConfigInfo.sourceGroupName = tci.sourceGroupName ?? "";
1538
+ tenantConfigInfo.targetGroupId = tci.targetGroupId ?? "";
1539
+ tenantConfigInfo.targetGroupName = tci.targetGroupName ?? "";
1270
1540
  tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1271
1541
  tenantConfigInfo.deltaToken = tci.deltaToken ?? "";
1272
1542
  tenantConfigInfo.configId = config!.id;
1543
+ tenantConfigInfo.batchId = tci.batchId ?? "";
1273
1544
  config!.tenants.push(tenantConfigInfo);
1274
1545
  });
1275
1546
  // ensure this workspace tracks this config
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mindline/sync",
3
3
  "type": "module",
4
- "version": "1.0.37",
4
+ "version": "1.0.39",
5
5
  "types": "index.d.ts",
6
6
  "exports": "./index.ts",
7
7
  "description": "sync is a node.js package encapsulating javscript classes required for configuring Mindline sync service.",
@@ -0,0 +1,23 @@
1
+ [
2
+ {
3
+ "Run": "3",
4
+ "Start": "2023-09-17T12:00:00.000-07:00",
5
+ "POST": "2023-09-17T12:00:09.500-07:00",
6
+ "Read": "2023-09-17T12:00:15.500-07:00",
7
+ "Write": "2023-09-17T12:01:30.000-07:00"
8
+ },
9
+ {
10
+ "Run": "2",
11
+ "Start": "2023-09-17T12:00:00.000-07:00",
12
+ "POST": "2023-09-17T12:00:09.500-07:00",
13
+ "Read": "2023-09-17T12:00:15.500-07:00",
14
+ "Write": "2023-09-17T12:01:30.000-07:00"
15
+ },
16
+ {
17
+ "Run": "1",
18
+ "Start": "2023-09-17T12:00:00.000-07:00",
19
+ "POST": "2023-09-17T12:00:09.500-07:00",
20
+ "Read": "2023-09-17T12:00:15.500-07:00",
21
+ "Write": "2023-09-17T12:01:30.000-07:00"
22
+ }
23
+ ]