@mindline/sync 1.0.37 → 1.0.38

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.d.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): void;
164
+ unstart(setMilestones): void;
165
+ post(setMilestones): void;
166
+ read(setMilestones): void;
167
+ write(setMilestones): void;
168
+ #initFromObjects(milestones: Milestones): 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;
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, setConfigSyncResult, setIdleText, setMilestones): void;
183
+ uninitializeProgressBar(setSyncProgress, setConfigSyncResult, setIdleText, setMilestones): void;
184
+ initializeSignalR(
185
+ config: Config | null | undefined,
186
+ syncPortalGlobalState: InitInfo | null,
187
+ batchIdArray: Array<Object>,
188
+ setRefreshDeltaTrigger,
189
+ setReadersTotal,
190
+ setReadersCurrent,
191
+ setWritersTotal,
192
+ setWritersCurrent,
193
+ setMilestones,
194
+ setConfigSyncResult,
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): 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): 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): 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): 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): 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;
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 = 0;
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,254 @@ 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, setConfigSyncResult, setIdleText, setMilestones): 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
+ if (this.milestoneArray.milestones[0].Write == null) {
746
+ this.milestoneArray.write(setMilestones);
541
747
  }
748
+ setConfigSyncResult(`finished sync, no updates for ${this.pb_idle} seconds`);
542
749
  }
543
- }
750
+ // if we get to 100, stop the timer, let SignalR or countdown timer finish sync
751
+ if (this.pb_progress < 100) {
752
+ this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
753
+ setSyncProgress(this.pb_progress);
754
+ }
755
+ }, 1000);
756
+ this.milestoneArray.start(setMilestones);
544
757
  }
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
- }
758
+ uninitializeProgressBar(setSyncProgress, setConfigSyncResult, setIdleText, setMilestones): void {
759
+ this.pb_startTS = 0;
760
+ this.pb_progress = 0;
761
+ setSyncProgress(this.pb_progress);
762
+ setConfigSyncResult("sync failed to execute");
763
+ this.pb_increment = 0;
764
+ clearInterval(this.pb_timer);
765
+ this.pb_timer = 0;
766
+ this.pb_idle = 0;
767
+ this.pb_idleMax = 0;
768
+ setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
769
+ this.milestoneArray.unstart(setMilestones);
770
+ }
771
+ initializeSignalR(
772
+ config: Config | null | undefined,
773
+ syncPortalGlobalState: InitInfo | null,
774
+ batchIdArray: Array<Object>,
775
+ setRefreshDeltaTrigger,
776
+ setReadersTotal,
777
+ setReadersCurrent,
778
+ setWritersTotal,
779
+ setWritersCurrent,
780
+ setMilestones,
781
+ setConfigSyncResult,
782
+ bClearLocalStorage: boolean
783
+ ): void {
784
+ // we have just completed a successful POST to startSync
785
+ this.milestoneArray.post(setMilestones);
786
+ setConfigSyncResult("started sync, waiting for updates...");
787
+ // re-initialize batch array with Configuration updated by the succcessful POST to startSync
788
+ this.init(config, syncPortalGlobalState, false);
556
789
  // define newMessage handler that can access *this*
557
790
  let handler = (message) => {
558
791
  console.log(message);
559
792
  let item = JSON.parse(message);
793
+ // reset the countdown timer every time we get a message
794
+ this.pb_idle = 0;
560
795
  // find the associated tenant for this SignalR message
561
- let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === item.TargetID);
796
+ let matchingPair: Object = batchIdArray.find((o: Object) => o.BatchId == item.TargetID);
797
+ if (matchingPair == null) {
798
+ console.log(`Batch ${item.TargetID} not found in batchIdArray.`);
799
+ debugger;
800
+ return;
801
+ }
802
+ let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === matchingPair.SourceId);
562
803
  if (tenantNode == null) { // null OR undefined
563
- console.log(`${item.TargetID} not found in BatchArray.`);
804
+ console.log(`Tenant ${matchingPair.SourceId} not found in BatchArray.`);
564
805
  debugger;
565
806
  return;
566
807
  }
567
- let writerNode: TenantNode|null = null;
568
- // process stats for this SignalR message
808
+ tenantNode.batchId = matchingPair.BatchId;
809
+ // process stats for this SignalR message batch
569
810
  let statsarray = item.Stats; // get the array of statistics
570
811
  let statskeys = Object.keys(statsarray); // get the keys of the array
571
812
  let statsvalues = Object.values(statsarray); // get the values of the array
572
813
  for (let j = 0; j < statskeys.length; j++) {
814
+ let bTotalCount = statskeys[j].endsWith("TotalCount");
815
+ let bCurrentCount = statskeys[j].endsWith("CurrentCount");
816
+ let bDeferredCount = statskeys[j].endsWith("DeferredCount");
573
817
  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}`);
818
+ // parse tid from Reader key
819
+ let tidRegexp = /Reader\/TID:(.+)\/TotalCount/;
820
+ if (bCurrentCount) tidRegexp = /Reader\/TID:(.+)\/CurrentCount/;
821
+ if (bDeferredCount) tidRegexp = /Reader\/TID:(.+)\/DeferredCount/;
822
+ let matchTID = statskeys[j].match(tidRegexp);
823
+ if (matchTID == null) {
824
+ console.log(`tid not found in ${statskeys[j]}.`);
825
+ debugger;
826
+ return;
827
+ }
828
+ if (bTotalCount) {
829
+ tenantNode.total = Math.max(Number(statsvalues[j]), tenantNode.total);
830
+ console.log(`----- ${tenantNode.name} TID: ${tenantNode.tid} batchId: ${tenantNode.batchId}`);
831
+ console.log(`----- ${tenantNode.name} Total To Read: ${tenantNode.total}`);
602
832
  }
603
- if (statskeys[j].endsWith("CurrentCount")) {
604
- tenantNode.read = Number(statsvalues[j]);
833
+ else {
834
+ tenantNode.read = Math.max(Number(statsvalues[j]), tenantNode.read);
605
835
  console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
606
836
  }
607
837
  }
608
838
  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
- }
839
+ // parse tid from Writer key
840
+ let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
841
+ if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
842
+ if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
843
+ let matchTID = statskeys[j].match(tidRegexp);
844
+ if (matchTID == null) {
845
+ console.log(`tid not found in ${statskeys[j]}.`);
846
+ debugger;
847
+ return;
648
848
  }
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}`);
849
+ // this Writer node should exist precisely under the Reader for this SignalR message
850
+ let writerNode: TenantNode = tenantNode.targets.find((t: TenantNode) => t.tid === matchTID[1]);
851
+ if (writerNode == null) {
852
+ console.log(`Writer ${tenantNode.name} not found under Reader ${tenantNode.name}.`);
853
+ debugger;
854
+ return;
683
855
  }
856
+ writerNode.batchId = matchingPair.BatchId;
857
+ if (bTotalCount) {
858
+ writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
859
+ console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
860
+ console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
861
+ }
862
+ else if (bCurrentCount) {
863
+ writerNode.written = Math.max(Number(statsvalues[j]), writerNode.written);
864
+ console.log(`----- ${writerNode.name} Total Written: ${writerNode.written}`);
865
+ }
866
+ else if (bDeferredCount) {
867
+ writerNode.deferred = Math.max(Number(statsvalues[j]), writerNode.deferred);
868
+ console.log(`----- ${writerNode.name} Total Deferred: ${writerNode.deferred}`);
869
+ }
870
+ else {
871
+ console.log(`unknown writer type`);
872
+ debugger;
873
+ return;
874
+ }
875
+ writerNode.update(writerNode.total, writerNode.read, writerNode.written, writerNode.deferred);
684
876
  }
685
877
  }
878
+ // update status based on all updates in this message
686
879
  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
- }
880
+ // for each message, enumerate nodes to assess completion state
881
+ let bReadingComplete: boolean = true;
882
+ let bWritingComplete: boolean = true;
883
+ let bWritingStarted: boolean = false;
884
+ let readerTotal: number = 0;
885
+ let readerCurrent: number = 0;
886
+ let writerTotal: number = 0;
887
+ let writerCurrent: number = 0;
888
+ this.tenantNodes.map((sourceTenantNode: TenantNode) => {
889
+ sourceTenantNode.targets.map((writerNode: TenantNode) => {
890
+ bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
891
+ bWritingStarted ||= (writerNode.total > 0 || writerNode.status != "not started");
892
+ writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
893
+ writerCurrent += writerNode.written;
894
+ });
895
+ bReadingComplete &&= (sourceTenantNode.status == "complete" || sourceTenantNode.status == "failed");
896
+ readerTotal += sourceTenantNode.total;
897
+ readerCurrent += sourceTenantNode.read;
898
+ });
899
+ // set linear gauge max and current values
900
+ setReadersTotal(readerTotal);
901
+ setReadersCurrent(readerCurrent);
902
+ setWritersTotal(Math.max(writerTotal, readerTotal));
903
+ setWritersCurrent(writerCurrent);
904
+ // because it is an important milestone, we always check if we have *just* completed reading
905
+ if (bReadingComplete && this.milestoneArray.milestones[0].Read == null) {
906
+ this.milestoneArray.read(setMilestones);
907
+ setConfigSyncResult("reading complete");
908
+ console.log(`Setting config sync result: "reading complete"`);
909
+ // trigger refresh delta tokens
910
+ setRefreshDeltaTrigger(true);
911
+ // change to % per second to complete in 7x as long as it took to get here
912
+ let readTS = Date.now();
913
+ let secsElapsed = (readTS - this.pb_startTS) / 1000;
914
+ let expectedPercentDone = 7;
915
+ let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
916
+ this.pb_increment = expectedPercentPerSecond;
917
+ console.log(`Setting increment: ${this.pb_increment}% per second`);
697
918
  }
698
- else {
919
+ // with that out of the way, is writing complete?
920
+ if (bWritingComplete) {
921
+ this.milestoneArray.write(setMilestones);
922
+ setConfigSyncResult("sync complete");
923
+ console.log(`Setting config sync result: "complete"`);
924
+ this.pb_progress = 99;
925
+ }
926
+ // if not, has writing even started?
927
+ else if (bWritingStarted) {
928
+ setConfigSyncResult("writing in progress");
929
+ console.log(`Setting config sync result: "writing in progress"`);
930
+ }
931
+ // else, we must be reading (unless we already completed reading)
932
+ else if (this.milestoneArray.milestones[0].Read == null){
699
933
  setConfigSyncResult("reading in progress");
700
934
  console.log(`Setting config sync result: "reading in progress"`);
701
935
  }
702
936
  }
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}`;
937
+ // start SignalR connection based on each batchId
938
+ batchIdArray.map((batchPair: Object) => {
939
+ const endpointUrl = `https://dev-signalrdispatcher-westus.azurewebsites.net/statsHub?statsId=${batchPair.BatchId}`;
940
+ console.log(`Creating SignalR Hub for TID: ${batchPair.SourceId} ${endpointUrl}`);
706
941
  const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
707
942
  .withUrl(endpointUrl)
708
- // .withAutomaticReconnect() TODO: full auto-reconnect implementation
943
+ .withAutomaticReconnect()
709
944
  .configureLogging(signalR.LogLevel.Information)
710
945
  .build();
711
946
  // when you get a message, process the message
712
947
  connection.on("newMessage", handler);
948
+ connection.onreconnecting(error => {
949
+ console.assert(connection.state === signalR.HubConnectionState.Reconnecting);
950
+ console.log(`Connection lost due to error "${error}". Reconnecting.`);
951
+ });
952
+ connection.onreconnected(connectionId => {
953
+ console.assert(connection.state === signalR.HubConnectionState.Connected);
954
+ console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
955
+ });
956
+ // restart when you get a close event
957
+ connection.onclose(async () => {
958
+ console.log(`Connection closing. Attempting restart.`);
959
+ await connection.start();
960
+ });
961
+ // start and display any caught exceptions in the console
713
962
  connection.start().catch(console.error);
714
963
  });
964
+ }
965
+ // start a sync cycle
966
+ async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): Promise<APIResult>
967
+ {
968
+ let result: APIResult = new APIResult();
969
+ if (this.tenantNodes == null || this.tenantNodes.length == 0) {
970
+ // we should not have an empty batch array for a test
971
+ debugger;
972
+ result.result = false;
973
+ result.error = "startSync: invalid parameters";
974
+ result.status = 500;
975
+ return result;
976
+ }
715
977
  // execute post to reader endpoint
716
978
  result = await readerPost(instance, authorizedUser, config);
717
979
  return result;
@@ -728,11 +990,11 @@ export class TenantNode {
728
990
  written: number;
729
991
  deferred: number;
730
992
  targets: TenantNode[];
731
- constructor(tid: string, name: string) {
993
+ constructor(tid: string, name: string, batchId: string) {
732
994
  this.expanded = false;
733
995
  this.name = name;
734
996
  this.tid = tid;
735
- this.batchId = "";
997
+ this.batchId = batchId;
736
998
  this.targets = new Array<TenantNode>();
737
999
  this.update(0, 0, 0, 0);
738
1000
  }
@@ -749,7 +1011,7 @@ export class TenantNode {
749
1011
  else if (this.written > 0) {
750
1012
  if (this.written + this.deferred < this.total) this.status = "in progress";
751
1013
  else if (this.written === this.total) this.status = "complete";
752
- else if (this.written + this.deferred === this.total) this.status = "failed";
1014
+ else if (this.written + this.deferred >= this.total) this.status = "failed";
753
1015
  }
754
1016
  }
755
1017
  }
@@ -826,7 +1088,7 @@ export function signIn(user: User, tasks: TaskArray): void {
826
1088
  tenantURL += "MicrosoftIdentity/Account/Challenge";
827
1089
  let url: URL = new URL(tenantURL);
828
1090
  url.searchParams.append("redirectUri", window.location.origin);
829
- url.searchParams.append("scope", "openid offline_access profile user.read contacts.read CrossTenantInformation.ReadBasic.All");
1091
+ url.searchParams.append("scope", "openid offline_access Directory.AccessAsUser.All CrossTenantInformation.ReadBasic.All");
830
1092
  url.searchParams.append("domainHint", "organizations");
831
1093
  if (user.oid !== "1") {
832
1094
  url.searchParams.append("loginHint", user.mail);
@@ -1173,7 +1435,7 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1173
1435
  returnedAdmins.map((item) => {
1174
1436
  // are we already tracking this user?
1175
1437
  let user: User | null = null;
1176
- let usIndex = ii.us.findIndex((u) => u.oid === item.userId);
1438
+ let usIndex = ii.us.findIndex((u) => (u.oid === item.userId || u.oid === item.email));
1177
1439
  if (usIndex === -1) {
1178
1440
  // start tracking
1179
1441
  let dummyIndex = ii.us.findIndex((u) => u.oid === "1");
@@ -1192,13 +1454,13 @@ function processReturnedAdmins(workspace: Workspace, ii: InitInfo, returnedAdmin
1192
1454
  user = ii.us.at(usIndex);
1193
1455
  }
1194
1456
  // refresh all the data available from the server
1195
- user.oid = item.userId;
1457
+ user.oid = item.userId ? item.userId : item.email;
1196
1458
  user.name = item.firstName;
1197
1459
  user.mail = item.email;
1198
1460
  user.tid = item.tenantId;
1199
1461
  // 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);
1462
+ let idx = workspace.associatedUsers.findIndex((u) => u === user.oid);
1463
+ if (idx == -1) workspace.associatedUsers.push(user.oid);
1202
1464
  });
1203
1465
  }
1204
1466
  function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTenants: Array<Object>) {
@@ -1227,7 +1489,12 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1227
1489
  tenant.tenantType = item.type.toLowerCase(); // should now be strings
1228
1490
  tenant.permissionType = item.permissionType.toLowerCase(); // should now be strings
1229
1491
  tenant.onboarded = item.isOnboarded ? "true" : "false";
1230
- tenant.authority = item.authority;
1492
+
1493
+ // canonicalize authority when getting it from config backend
1494
+ const regex = /^(https:\/\/login.microsoftonline.(?:us|com)\/)organizations\/v2.0$/;
1495
+ const regexMatch = item.authority.match(regex);
1496
+ tenant.authority = regexMatch ? regexMatch[1] : item.authority;
1497
+
1231
1498
  tenant.readServicePrincipal = item.readServicePrincipal;
1232
1499
  tenant.writeServicePrincipal = item.writeServicePrincipal;
1233
1500
  // ensure this workspace tracks this tenant
@@ -1265,11 +1532,14 @@ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConf
1265
1532
  item.tenants.map((tci) => {
1266
1533
  let tenantConfigInfo = new TenantConfigInfo();
1267
1534
  tenantConfigInfo.tid = tci.tenantId;
1268
- tenantConfigInfo.sourceGroupId = tci.sourceGroupId;
1269
- tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1535
+ tenantConfigInfo.sourceGroupId = tci.sourceGroupId ?? "";
1536
+ tenantConfigInfo.sourceGroupName = tci.sourceGroupName ?? "";
1537
+ tenantConfigInfo.targetGroupId = tci.targetGroupId ?? "";
1538
+ tenantConfigInfo.targetGroupName = tci.targetGroupName ?? "";
1270
1539
  tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1271
1540
  tenantConfigInfo.deltaToken = tci.deltaToken ?? "";
1272
1541
  tenantConfigInfo.configId = config!.id;
1542
+ tenantConfigInfo.batchId = tci.batchId ?? "";
1273
1543
  config!.tenants.push(tenantConfigInfo);
1274
1544
  });
1275
1545
  // 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.38",
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
+ ]