@mindline/sync 1.0.35 → 1.0.37

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,6 +2,5 @@
2
2
  "ExpandedNodes": [
3
3
  ""
4
4
  ],
5
- "SelectedNode": "\\index.ts",
6
5
  "PreviewInSolutionExplorer": false
7
6
  }
package/.vs/slnx.sqlite CHANGED
Binary file
Binary file
package/hybridspa.ts CHANGED
@@ -36,11 +36,17 @@ export const graphConfig = {
36
36
  graphGroupsEndpoint: "https://graph.microsoft.com/v1.0/groups",
37
37
  graphMailEndpoint: "https://graph.microsoft.com/v1.0/me/messages",
38
38
  graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
39
- graphTenantByDomainEndpoint:
40
- "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByDomainName",
41
- graphTenantByIdEndpoint:
42
- "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId",
43
39
  graphUsersEndpoint: "https://graph.microsoft.com/v1.0/users",
40
+ // sovereign cloud tenant info endpoints
41
+ graphTenantByDomainPredicate: "beta/tenantRelationships/findTenantInformationByDomainName",
42
+ graphTenantByIdPredicate: "beta/tenantRelationships/findTenantInformationByTenantId",
43
+ // authority values are based on the well-known OIDC auth endpoints
44
+ authorityWW: "https://login.microsoftonline.com/",
45
+ authorityWWRegex: /^(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$/,
46
+ authorityUS: "https://login.microsoftonline.us/",
47
+ 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
+ authorityCN: "https://login.partner.microsoftonline.cn/",
49
+ 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$/,
44
50
  // reader endpoint to trigger sync start
45
51
  readerStartSyncEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/startSync/",
46
52
  readerApiEndpoint: "https://dev-fn-reader-westus.azurewebsites.net/api/lookup/"
@@ -215,20 +221,9 @@ export async function adminPost(
215
221
  workspaceId: string
216
222
  ): Promise<APIResult> {
217
223
  let result: APIResult = new APIResult();
218
- if (
219
- user.mail == null ||
220
- user.mail === "" ||
221
- user.authority == null ||
222
- user.authority === "" ||
223
- user.tid == null ||
224
- user.tid === "" ||
225
- user.companyName == null ||
226
- user.companyName === "" ||
227
- user.companyDomain == null ||
228
- user.companyDomain === ""
229
- ) {
224
+ if (user.mail == "" || user.authority == "" || user.tid === "") {
230
225
  result.result = false;
231
- result.error = "configPost: invalid config ID";
226
+ result.error = "adminPost: invalid argument";
232
227
  result.status = 500;
233
228
  return result;
234
229
  }
@@ -863,14 +858,12 @@ export async function readerPost(
863
858
  return result;
864
859
  }
865
860
  // create reader endpoint with config ID
866
- //let readerEndpoint: string = graphConfig.readerStartSyncEndpoint + config.id;
867
- let readerEndpoint: string = graphConfig.readerApiEndpoint + config.id;
861
+ let readerEndpoint: string = graphConfig.readerStartSyncEndpoint + config.id;
868
862
  // create headers
869
863
  const headers = await defineHeaders(instance, authorizedUser);
870
864
  // make reader endpoint call
871
865
  let options = { method: "POST", headers: headers };
872
866
  try {
873
- debugger;
874
867
  console.log("Attempting POST to /startSync: " + readerEndpoint);
875
868
  let response = await fetch(readerEndpoint, options);
876
869
  if (response.status === 200 && response.statusText === "OK") {
package/index.d.ts CHANGED
@@ -1,187 +1,188 @@
1
1
  import { IPublicClientApplication } from "@azure/msal-browser";
2
2
 
3
3
  declare module "@mindline/sync" {
4
- export function sum(a: number, b: number): number;
5
- export function helloNpm(): string;
4
+ export function sum(a: number, b: number): number;
5
+ export function helloNpm(): string;
6
6
 
7
- export class Group {
8
- id: string;
9
- displayName: string;
10
- description: string;
11
- }
12
- // admin
13
- export class User {
14
- oid: string; // from AAD ID token
15
- name: string; // from AAD ID token
16
- mail: string; // from AAD ID token TODO: preferred_username *may* differ from UPN, may differ from mail
17
- authority: string; // from AAD auth response - cloud instance login endpoint
18
- tid: string; // from AAD ID token
19
- companyName: string; // findTenantInformationByTenantId TODO: process changes to company name
20
- companyDomain: string; // findTenantInformationByTenantId TODO: process changes to company name
21
- associatedWorkspaces: string[];
22
- workspaceIDs: string;
23
- session: string;
24
- spacode: string;
25
- accessToken: string;
26
- loginHint: string;
27
- scopes: string[];
28
- authTS: Date;
29
- constructor();
30
- }
31
- // tenant (Azure AD tenant, AD domain, Google workspace)
32
- export enum TenantType {
33
- invalid = 0,
34
- aad = 1,
35
- ad = 2,
36
- googleworkspace = 3
37
- }
38
- type TenantTypeStrings = keyof typeof TenantType;
39
- export enum TenantPermissionType {
40
- read = 1,
41
- write = 2,
42
- notassigned = 3
43
- }
44
- type TenantPermissionTypeStrings = keyof typeof TenantPermissionType;
45
- export class Tenant {
46
- tid: string; // from AAD ID token
47
- name: string; // findTenantInformationByTenantId
48
- domain: string; // findTenantInformationByTenantId
49
- tenantType: TenantTypeStrings; // always "aad" for now
50
- permissionType: TenantPermissionTypeStrings; // read/write/notassigned
51
- onboarded: string; // have we onboarded this tenant? "true" or "false"
52
- authority: string; // from AAD ID auth response
53
- readServicePrincipal: string; // from AAD consent
54
- writeServicePrincipal: string; // from AAD consent
55
- workspaceIDs: string;
56
- constructor();
57
- }
58
- // config
59
- export enum TenantConfigType {
60
- source = 1,
61
- target = 2,
62
- sourcetarget = 3
63
- }
64
- export type TenantConfigTypeStrings = keyof typeof TenantConfigType;
65
- export class TenantConfigInfo {
66
- tid: string; // tenant identifier
67
- sourceGroupId: string; // source group - we can configure source group for reading
68
- sourceGroupName: string; // source group - we can configure source group for reading
69
- configurationTenantType: TenantConfigTypeStrings;
70
- deltaToken: string;
71
- filesWritten: number;
72
- }
73
- export class Config {
74
- id: string;
75
- workspaceId: string;
76
- name: string;
77
- description: string;
78
- tenants: TenantConfigInfo[];
79
- isEnabled: boolean;
80
- workspaceIDs: string;
81
- constructor();
82
- }
83
- // class to group Users, Tenants, and Configs
84
- export class Workspace {
85
- id: string;
86
- name: string;
87
- associatedUsers: string[];
88
- associatedTenants: string[];
89
- associatedConfigs: string[];
90
- constructor();
91
- }
92
- export class InitInfo {
93
- us: User[];
94
- ts: Tenant[];
95
- cs: Config[];
96
- ws: Workspace[];
97
- constructor(bClearLocalStorage: boolean);
98
- init(bClearLocalStorage: boolean): void;
99
- save(): void;
100
- tagWithWorkspaces(): boolean;
101
- }
102
- export type TaskType = "initialization" |
7
+ export class Group {
8
+ id: string;
9
+ displayName: string;
10
+ description: string;
11
+ }
12
+ // admin
13
+ export class User {
14
+ oid: string; // from AAD ID token
15
+ name: string; // from AAD ID token
16
+ mail: string; // from AAD ID token TODO: preferred_username *may* differ from UPN, may differ from mail
17
+ authority: string; // from AAD auth response - cloud instance login endpoint
18
+ tid: string; // from AAD ID token
19
+ companyName: string; // findTenantInformationByTenantId TODO: process changes to company name
20
+ companyDomain: string; // findTenantInformationByTenantId TODO: process changes to company name
21
+ associatedWorkspaces: string[];
22
+ workspaceIDs: string;
23
+ session: string;
24
+ spacode: string;
25
+ accessToken: string;
26
+ loginHint: string;
27
+ scopes: string[];
28
+ authTS: Date;
29
+ constructor();
30
+ }
31
+ // tenant (Azure AD tenant, AD domain, Google workspace)
32
+ export enum TenantType {
33
+ invalid = 0,
34
+ aad = 1,
35
+ ad = 2,
36
+ googleworkspace = 3
37
+ }
38
+ type TenantTypeStrings = keyof typeof TenantType;
39
+ export enum TenantPermissionType {
40
+ read = 1,
41
+ write = 2,
42
+ notassigned = 3
43
+ }
44
+ type TenantPermissionTypeStrings = keyof typeof TenantPermissionType;
45
+ export class Tenant {
46
+ tid: string; // from AAD ID token
47
+ name: string; // findTenantInformationByTenantId
48
+ domain: string; // findTenantInformationByTenantId
49
+ tenantType: TenantTypeStrings; // always "aad" for now
50
+ permissionType: TenantPermissionTypeStrings; // read/write/notassigned
51
+ onboarded: string; // have we onboarded this tenant? "true" or "false"
52
+ authority: string; // from AAD ID auth response
53
+ readServicePrincipal: string; // from AAD consent
54
+ writeServicePrincipal: string; // from AAD consent
55
+ workspaceIDs: string;
56
+ constructor();
57
+ }
58
+ // config
59
+ export enum TenantConfigType {
60
+ source = 1,
61
+ target = 2,
62
+ sourcetarget = 3
63
+ }
64
+ export type TenantConfigTypeStrings = keyof typeof TenantConfigType;
65
+ export class TenantConfigInfo {
66
+ tid: string; // tenant identifier
67
+ sourceGroupId: string; // source group - we can configure source group for reading
68
+ sourceGroupName: string; // source group - we can configure source group for reading
69
+ configurationTenantType: TenantConfigTypeStrings;
70
+ deltaToken: string;
71
+ filesWritten: number;
72
+ configId: string;
73
+ }
74
+ export class Config {
75
+ id: string;
76
+ workspaceId: string;
77
+ name: string;
78
+ description: string;
79
+ tenants: TenantConfigInfo[];
80
+ isEnabled: boolean;
81
+ workspaceIDs: string;
82
+ constructor();
83
+ }
84
+ // class to group Users, Tenants, and Configs
85
+ export class Workspace {
86
+ id: string;
87
+ name: string;
88
+ associatedUsers: string[];
89
+ associatedTenants: string[];
90
+ associatedConfigs: string[];
91
+ constructor();
92
+ }
93
+ export class InitInfo {
94
+ us: User[];
95
+ ts: Tenant[];
96
+ cs: Config[];
97
+ ws: Workspace[];
98
+ constructor(bClearLocalStorage: boolean);
99
+ init(bClearLocalStorage: boolean): void;
100
+ save(): void;
101
+ tagWithWorkspaces(): boolean;
102
+ }
103
+ export type TaskType = "initialization" |
103
104
  "authenticate user" |
104
105
  "reload React" |
105
106
  "PUT access token" |
106
107
  "GET tenant details" |
107
108
  "POST config init" |
108
109
  "GET workspaces" |
109
- "onboard tenant" |
110
- "create 2nd tenant" |
111
- "invite 2nd admin" |
112
- "onboard 2nd tenant" |
113
- "create config";
114
- export class TaskArray {
115
- tasks: Task[];
116
- constructor(bClearLocalStorage: boolean);
117
- init(bClearLocalStorage: boolean): void;
118
- setTaskStart(taskType: TaskType, startDate: Date): void;
119
- setTaskEnd(taskType: TaskType, startDate: Date, status: string): void;
120
- }
121
- export class Task {
122
- id: number;
123
- task: string;
124
- start: Date;
125
- startDisplay: string;
126
- end: Date;
127
- endDisplay: string;
128
- elapsedDisplay: string;
129
- expected: number;
130
- status: string;
131
- expanded: boolean;
132
- subtasks: Task[];
133
- setEnd(endDate: Date): void;
134
- setStart(startDate: Date): void;
135
- }
136
- export class BatchArray {
137
- tenantNodes: TenantNode[];
138
- constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
139
- // populate tenantNodes based on config tenants
140
- init(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean): void;
141
- startSync(instance: IPublicClientApplication, authorizedUser: User|null|undefined, config: Config|null|undefined): void;
142
- }
143
- export class TenantNode {
144
- expanded: boolean;
145
- status: string;
146
- name: string;
147
- tid: string;
148
- total: number;
149
- read: number;
150
- written: number;
151
- deferred: number;
152
- targets: TenantNode[];
153
- constructor(tid: string, name: string);
154
- update(total: number, read: number, written: number, deferred: number): void;
155
- }
156
- export class APIResult {
157
- result: boolean;
158
- status: number;
159
- error: string;
160
- constructor();
161
- }
162
- //
163
- // Azure AD Graph API
164
- //
165
- export function groupGet(tenant: Tenant, groupid: string): Promise<{group: string, error: string}>;
166
- export function groupsGet(tenant: Tenant, groupSearchString: string): Promise<{groups: Group[], error: string}>;
167
- export function signIn(user: User, tasks: TaskArray): void;
168
- export function signInIncrementally(user: User, scope: string): void;
169
- export function signOut(user: User): void;
170
- export function tenantRelationshipsGetByDomain(loggedInuser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): boolean;
171
- export function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): boolean;
172
- export function usersGet(tenant: Tenant): {users: string[], error: string};
173
- //
174
- // Mindline Config API
175
- //
176
- export function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): APIResult;
177
- export function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): APIResult;
178
- export function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): APIResult;
179
- export function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): APIResult;
180
- export function tenantComplete(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, debug: boolean): APIResult;
181
- export function tenantRemove(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string, debug: boolean): APIResult;
182
- export function userAdd(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): APIResult;
110
+ "onboard tenant" |
111
+ "create 2nd tenant" |
112
+ "invite 2nd admin" |
113
+ "onboard 2nd tenant" |
114
+ "create config";
115
+ export class TaskArray {
116
+ tasks: Task[];
117
+ constructor(bClearLocalStorage: boolean);
118
+ init(bClearLocalStorage: boolean): void;
119
+ setTaskStart(taskType: TaskType, startDate: Date): void;
120
+ setTaskEnd(taskType: TaskType, startDate: Date, status: string): void;
121
+ }
122
+ export class Task {
123
+ id: number;
124
+ task: string;
125
+ start: Date;
126
+ startDisplay: string;
127
+ end: Date;
128
+ endDisplay: string;
129
+ elapsedDisplay: string;
130
+ expected: number;
131
+ status: string;
132
+ expanded: boolean;
133
+ subtasks: Task[];
134
+ setEnd(endDate: Date): void;
135
+ setStart(startDate: Date): void;
136
+ }
137
+ export class BatchArray {
138
+ tenantNodes: TenantNode[];
139
+ constructor(config: Config|null, syncPortalGlobalState: InitInfo|null, bClearLocalStorage: boolean);
140
+ // 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;
143
+ }
144
+ export class TenantNode {
145
+ expanded: boolean;
146
+ status: string;
147
+ name: string;
148
+ tid: string;
149
+ batchId: string;
150
+ total: number;
151
+ read: number;
152
+ written: number;
153
+ deferred: number;
154
+ targets: TenantNode[];
155
+ constructor(tid: string, name: string);
156
+ update(total: number, read: number, written: number, deferred: number): void;
157
+ }
158
+ export class APIResult {
159
+ result: boolean;
160
+ status: number;
161
+ error: string;
162
+ constructor();
163
+ }
164
+ //
165
+ // Azure AD Graph API
166
+ //
167
+ export function groupGet(tenant: Tenant, groupid: string): Promise<{group: string, error: string}>;
168
+ export function groupsGet(tenant: Tenant, groupSearchString: string): Promise<{groups: Group[], error: string}>;
169
+ export function signIn(user: User, tasks: TaskArray): void;
170
+ export function signInIncrementally(user: User, scope: string): void;
171
+ export function signOut(user: User): void;
172
+ export function tenantRelationshipsGetByDomain(loggedInuser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): boolean;
173
+ export function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): boolean;
174
+ export function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean>;
175
+ export function usersGet(tenant: Tenant): { users: string[], error: string };
176
+ //
177
+ // Mindline Config API
178
+ //
179
+ export function configEdit(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): APIResult;
180
+ export function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): APIResult;
181
+ export function configsRefresh(instance: IPublicClientApplication, authorizedUser: User, workspaceId: string, ii: InitInfo, debug: boolean): APIResult;
182
+ export function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): APIResult;
183
+ export function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): APIResult;
184
+ export function tenantComplete(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, debug: boolean): APIResult;
185
+ export function tenantRemove(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string, debug: boolean): APIResult;
186
+ export function userAdd(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): APIResult;
183
187
  export function userRemove(instance: IPublicClientApplication, authorizedUser: User, user: User, workspaceId: string): APIResult;
184
- //
185
- // Mindline Sync API
186
- //
187
188
  }
package/index.ts CHANGED
@@ -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 { log } from "console";
12
13
  const FILTER_FIELD = "workspaceIDs";
13
14
  // called by unit tests
14
15
  export function sum(a: number, b: number): number {
@@ -42,7 +43,7 @@ export class User {
42
43
  this.oid = "";
43
44
  this.name = "";
44
45
  this.mail = "";
45
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
46
+ this.authority = "";
46
47
  this.tid = "";
47
48
  this.companyName = "";
48
49
  this.companyDomain = "";
@@ -87,12 +88,20 @@ export class Tenant {
87
88
  this.tenantType = "aad";
88
89
  this.permissionType = "notassigned";
89
90
  this.onboarded = "false";
90
- this.authority = "https://login.microsoftonline.com/organizations/v2.0";
91
+ this.authority = "";
91
92
  this.readServicePrincipal = "";
92
93
  this.writeServicePrincipal = "";
93
94
  this.workspaceIDs = "";
94
95
  }
95
96
  }
97
+ function getGraphEndpoint(authority: string): string {
98
+ switch (authority) {
99
+ case graphConfig.authorityWW: return "https://graph.microsoft.com/";
100
+ case graphConfig.authorityUS: return "https://graph.microsoft.us/";
101
+ case graphConfig.authorityCN: return "https://microsoftgraph.chinacloudapi.cn/";
102
+ default: debugger; return "";
103
+ }
104
+ }
96
105
  export enum TenantConfigType {
97
106
  source = 1,
98
107
  target = 2,
@@ -106,6 +115,7 @@ export class TenantConfigInfo {
106
115
  configurationTenantType: TenantConfigTypeStrings;
107
116
  deltaToken: string;
108
117
  filesWritten: number;
118
+ configId: string;
109
119
  constructor() {
110
120
  this.tid = "";
111
121
  this.sourceGroupId = "";
@@ -113,6 +123,7 @@ export class TenantConfigInfo {
113
123
  this.configurationTenantType = "source";
114
124
  this.deltaToken = "";
115
125
  this.filesWritten = 0;
126
+ this.configId = "";
116
127
  }
117
128
  }
118
129
  export class Config {
@@ -531,134 +542,179 @@ export class BatchArray {
531
542
  }
532
543
  }
533
544
  }
534
- /*
535
- monitorSyncProgress(): void {
536
- const connection = new signalR.HubConnectionBuilder()
537
- .withUrl("https://dev-signalrdispatcher-westus.azurewebsites.net/?statsId=df9c2e0a-f6fe-43bb-a155-d51f66dffe0e")
538
- .configureLogging(signalR.LogLevel.Information)
539
- .build();
540
-
541
- // when you get a message
542
- connection.on("newMessage", function (message) {
543
- console.log(message); // log the message
544
- const item = JSON.parse(message); //parse it into an object
545
- const statsarray = item.Stats; // get the array of statistics
546
- const statskeys = Object.keys(statsarray); // get the keys of the array
547
- const statsvalues = Object.values(statsarray); // get the values of the array
548
- let statistics = ""; // initialize statistics
549
- let total = 1;
550
- let currentR = 0;
551
- let currentW = 0;
552
- let currentD = 0;
553
- for (let j = 0; j < statskeys.length; j++) { // store the TotalCount and store as total
554
- if (statskeys[j].endsWith("TotalCount")) {
555
- total = statsvalues[j];
556
- }
557
- if (statskeys[j].endsWith("CurrentCount")) { // store the Reader value as currentR
558
- if (statskeys[j].startsWith("Reader")) {
559
- currentR = statsvalues[j];
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
+ }
556
+ // define newMessage handler that can access *this*
557
+ let handler = (message) => {
558
+ console.log(message);
559
+ let item = JSON.parse(message);
560
+ // find the associated tenant for this SignalR message
561
+ let tenantNode: TenantNode = this.tenantNodes.find((t: TenantNode) => t.tid === item.TargetID);
562
+ if (tenantNode == null) { // null OR undefined
563
+ console.log(`${item.TargetID} not found in BatchArray.`);
564
+ debugger;
565
+ return;
566
+ }
567
+ let writerNode: TenantNode|null = null;
568
+ // process stats for this SignalR message
569
+ let statsarray = item.Stats; // get the array of statistics
570
+ let statskeys = Object.keys(statsarray); // get the keys of the array
571
+ let statsvalues = Object.values(statsarray); // get the values of the array
572
+ for (let j = 0; j < statskeys.length; j++) {
573
+ 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}`);
560
602
  }
561
- if (statskeys[j].startsWith("Writer")) { // store the Writer value as currentW
562
- currentW = statsvalues[j];
603
+ if (statskeys[j].endsWith("CurrentCount")) {
604
+ tenantNode.read = Number(statsvalues[j]);
605
+ console.log(`----- ${tenantNode.name} Currently Read: ${tenantNode.read}`);
563
606
  }
564
607
  }
565
- if (statskeys[j].endsWith("DeferredCount")) { // store the deferred count
566
- currentD = statsvalues[j];
608
+ 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
+ }
648
+ }
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}`);
683
+ }
567
684
  }
568
- statistics = statistics + statskeys[j] + "=" + statsvalues[j] + "<br />" //
569
- }
570
- updateProgress(total, currentR, currentW, currentD);
571
- myFunction(item.TargetID, statistics);
572
- });
573
-
574
- connection.start().catch(console.error);
575
-
576
- function myFunction(targetid, str) {
577
- const table = document.getElementById("the-table");
578
- let row = document.getElementById(targetid);
579
- if (row) {
580
- row.cells[1].innerHTML = "<code>" + str + "</ code>";
581
- } else {
582
- row = table.insertRow(1);
583
- row.id = targetid;
584
- let cell1 = row.insertCell(0);
585
- let cell2 = row.insertCell(1);
586
- let caption = (targetid === "00000000-0000-0000-0000-000000000000") ? "<B>Status of ServiceBus QUEUES</B>" : targetid;
587
- cell1.innerHTML = "<code>" + caption + "</ code>";
588
- cell2.innerHTML = "<code>" + str + "</ code>";
589
685
  }
590
- }
591
-
592
- function updateProgress(total, currentR, currentW, currentD) {
593
- updateRead(total, currentR);
594
- updateWrite(total, currentW, currentD);
595
- }
596
-
597
- function updateRead(total, current) {
598
- const element = document.getElementById("readBar");
599
- const width = Math.round(100 * current / total);
600
- element.style.width = width + '%';
601
- element.innerHTML = "R=" + width + '%';
602
- }
603
-
604
- function updateWrite(total, w, d) {
605
- const elementW = document.getElementById("writeBar");
606
- const widthW = Math.round(100 * w / total);
607
- elementW.style.width = widthW + '%';
608
- elementW.innerHTML = "W=" + widthW + '%';
609
-
610
- const elementD = document.getElementById("deferBar");
611
- let width = 0;
612
- let widthScaled = 0;
613
- if (d > 0) {
614
- width = Math.round(100 * d / total) + 1;
615
- if (width < 10) {
616
- widthScaled = width * 10;
617
- }
618
- else if (width < 20) {
619
- widthScaled = width * 5;
686
+ 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"`);
620
692
  }
621
693
  else {
622
- widthScaled = width;
694
+ setConfigSyncResult("writing in progress");
695
+ console.log(`Setting config sync result: "writing in progress"`);
623
696
  }
624
697
  }
625
- if (widthScaled + widthW >= 99) {
626
- widthScaled = 99 - widthW;
627
- }
628
- elementD.style.width = widthScaled + '%';
629
- elementD.innerHTML = width + '%';
630
- document.getElementById("deferred").innerHTML = 'Deferred:' + width + '% (' + d + ' of ' + total + ')';
631
- // cycle through desired states for sources and targets
632
- if (this.tenantNodes != null) {
633
- this.tenantNodes.map((sourceTenantNode: TenantNode) => {
634
- if (sourceTenantNode.read == 0) sourceTenantNode.update(100, 50, 0, 0);
635
- else if (sourceTenantNode.read == 50) sourceTenantNode.update(100, 100, 0, 0);
636
- else sourceTenantNode.update(0, 0, 0, 0);
637
- if (sourceTenantNode.targets != null) {
638
- sourceTenantNode.targets.map((targetTenantNode: TenantNode) => {
639
- if (targetTenantNode.written == 0) targetTenantNode.update(100, 0, 50, 0);
640
- else if (targetTenantNode.written == 50) targetTenantNode.update(100, 0, 100, 0);
641
- else if (targetTenantNode.written == 100) targetTenantNode.update(100, 0, 99, 1);
642
- else targetTenantNode.update(0, 0, 0, 0);
643
- });
644
- }
645
- });
698
+ else {
699
+ setConfigSyncResult("reading in progress");
700
+ console.log(`Setting config sync result: "reading in progress"`);
646
701
  }
647
702
  }
648
- }
649
- */
650
- // cycle through test state machine
651
- startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: Config | null | undefined): void {
652
- if (this.tenantNodes == null || this.tenantNodes.length == 0) {
653
- // we should not have an empty batch array for a test
654
- debugger;
655
- }
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}`;
706
+ const connection: signalR.HubConnection = new signalR.HubConnectionBuilder()
707
+ .withUrl(endpointUrl)
708
+ // .withAutomaticReconnect() TODO: full auto-reconnect implementation
709
+ .configureLogging(signalR.LogLevel.Information)
710
+ .build();
711
+ // when you get a message, process the message
712
+ connection.on("newMessage", handler);
713
+ connection.start().catch(console.error);
714
+ });
656
715
  // execute post to reader endpoint
657
- readerPost(instance, authorizedUser, config);
658
-
659
- // start SignalR connection
660
- //debugger;
661
- //monitorSyncProgress();
716
+ result = await readerPost(instance, authorizedUser, config);
717
+ return result;
662
718
  }
663
719
  }
664
720
  export class TenantNode {
@@ -666,6 +722,7 @@ export class TenantNode {
666
722
  status: string;
667
723
  name: string;
668
724
  tid: string;
725
+ batchId: string;
669
726
  total: number;
670
727
  read: number;
671
728
  written: number;
@@ -675,6 +732,7 @@ export class TenantNode {
675
732
  this.expanded = false;
676
733
  this.name = name;
677
734
  this.tid = tid;
735
+ this.batchId = "";
678
736
  this.targets = new Array<TenantNode>();
679
737
  this.update(0, 0, 0, 0);
680
738
  }
@@ -806,9 +864,9 @@ export function signOut(user: User): void {
806
864
  export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant: Tenant, instance: IPublicClientApplication, debug: boolean): Promise<boolean> {
807
865
  if (debug) debugger;
808
866
  // do we already have a valid tenant name? if so, nothing to add
809
- if (typeof tenant.name !== 'undefined' && tenant.name !== "") return false;
867
+ if (tenant.name != null && tenant.name !== "") return false;
810
868
  // if needed, retrieve and cache access token
811
- if (typeof loggedInUser.accessToken === 'undefined' || loggedInUser.accessToken === "") {
869
+ if (loggedInUser.accessToken != null && loggedInUser.accessToken === "") {
812
870
  console.log(`tenantRelationshipsGetByDomain called with invalid logged in user: ${loggedInUser.name}`);
813
871
  try {
814
872
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: loggedInUser.spacode });
@@ -828,29 +886,32 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
828
886
  // make tenant endpoint call
829
887
  try {
830
888
  // create tenant info endpoint
831
- var tenantEndpoint = graphConfig.graphTenantByDomainEndpoint;
889
+ var tenantEndpoint = getGraphEndpoint(tenant.authority) + graphConfig.graphTenantByDomainPredicate;
832
890
  tenantEndpoint += "(domainName='";
833
891
  tenantEndpoint += tenant.domain;
834
892
  tenantEndpoint += "')";
835
893
  console.log("Attempting GET from /findTenantInformationByDomainName:", tenantEndpoint);
836
894
  let response = await fetch(tenantEndpoint, options);
837
- let data = await response.json();
838
- if (data) {
839
- if (typeof data.error !== "undefined") {
840
- console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
841
- return false;
895
+ if (response.status == 200 && response.statusText == "OK") {
896
+ let data = await response.json();
897
+ if (data) {
898
+ if (data.error != null) {
899
+ debugger;
900
+ console.log("Failed GET from /findTenantInformationByDomainName: ", data.error.message);
901
+ return false;
902
+ }
903
+ else if (data.displayName != null && data.displayName !== "") {
904
+ // set domain information on passed tenant
905
+ tenant.tid = data.tenantId;
906
+ tenant.name = data.displayName;
907
+ console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
908
+ return true; // success, need UX to re-render
909
+ }
842
910
  }
843
- else if (typeof data.displayName !== undefined && data.displayName !== "") {
844
- // set domain information on passed tenant
845
- tenant.tid = data.tenantId;
846
- tenant.name = data.displayName;
847
- console.log("Successful GET from /findTenantInformationByDomainName: ", data.displayName);
848
- return true; // success, need UX to re-render
911
+ else {
912
+ console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
849
913
  }
850
914
  }
851
- else {
852
- console.log("Failed to GET from /findTenantInformationByTenantId: ", tenantEndpoint);
853
- }
854
915
  }
855
916
  catch (error: any) {
856
917
  console.log("Failed to GET from /findTenantInformationByTenantId: ", error);
@@ -862,9 +923,9 @@ export async function tenantRelationshipsGetByDomain(loggedInUser: User, tenant:
862
923
  export async function tenantRelationshipsGetById(user: User, ii: InitInfo, instance: IPublicClientApplication, tasks: TaskArray, debug: boolean): Promise<boolean> {
863
924
  if (debug) debugger;
864
925
  // do we already have a valid company name? if so, nothing to add, no need for UX to re-render
865
- if (typeof user.companyName !== 'undefined' && user.companyName !== "") return false;
926
+ if (user.companyName != "") return false;
866
927
  // if needed, retrieve and cache access token
867
- if (typeof user.accessToken === 'undefined' || user.accessToken === "") {
928
+ if (user.accessToken === "") {
868
929
  try {
869
930
  let response: AuthenticationResult = await instance.acquireTokenByCode({ code: user.spacode });
870
931
  user.accessToken = response.accessToken; // cache access token
@@ -883,7 +944,7 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
883
944
  // make tenant endpoint call
884
945
  try {
885
946
  // create tenant info endpoint
886
- var tenantEndpoint = graphConfig.graphTenantByIdEndpoint;
947
+ var tenantEndpoint = getGraphEndpoint(user.authority) + graphConfig.graphTenantByIdPredicate;
887
948
  tenantEndpoint += "(tenantId='";
888
949
  tenantEndpoint += user.tid;
889
950
  tenantEndpoint += "')";
@@ -922,6 +983,54 @@ export async function tenantRelationshipsGetById(user: User, ii: InitInfo, insta
922
983
  tasks.setTaskEnd("GET tenant details", new Date(), "failed");
923
984
  return false; // failed, no need for UX to re-render
924
985
  }
986
+ //tenantUnauthenticatedLookup (from https://gettenantpartitionweb.azurewebsites.net/js/gettenantpartition.js)
987
+ export async function tenantUnauthenticatedLookup(tenant: Tenant, debug: boolean): Promise<boolean> {
988
+ if (debug) debugger;
989
+ // do we already have a valid tenant ID? if so, nothing to add
990
+ if (tenant.tid !== "") return false;
991
+ // do we not have a valid domain? if so, nothing to lookup
992
+ if (tenant.domain == "") return false;
993
+ // prepare the 3 endpoints and corresponding regular expressions
994
+ let endpoints: string[] = [graphConfig.authorityWW, graphConfig.authorityUS, graphConfig.authorityCN];
995
+ let regexes: RegExp[] = [graphConfig.authorityWWRegex, graphConfig.authorityUSRegex, graphConfig.authorityCNRegex];
996
+ // make unauthenticated well-known openid endpoint call(s)
997
+ let response = null;
998
+ try {
999
+ for (let j = 0; j < 3; j++) {
1000
+ // create well-known openid endpoint
1001
+ var openidEndpoint = endpoints[j];
1002
+ openidEndpoint += tenant.domain;
1003
+ openidEndpoint += "/.well-known/openid-configuration";
1004
+ console.log("Attempting GET from openid well-known endpoint: ", openidEndpoint);
1005
+ response = await fetch(openidEndpoint);
1006
+ if (response.status == 200 && response.statusText == "OK") {
1007
+ let data = await response.json();
1008
+ if (data) {
1009
+ // store tenant ID and authority
1010
+ var tenantAuthEndpoint = data.authorization_endpoint;
1011
+ var authMatches = tenantAuthEndpoint.match(regexes[j]);
1012
+ tenant.tid = authMatches[2];
1013
+ tenant.authority = authMatches[1]; // USGov tenants are registered in WW with USGov authority values!
1014
+ console.log("Successful GET from openid well-known endpoint");
1015
+ return true; // success, need UX to re-render
1016
+ }
1017
+ else {
1018
+ console.log(`Failed JSON parse of openid well-known endpoint response ${openidEndpoint}.`);
1019
+ }
1020
+ }
1021
+ else {
1022
+ console.log(`Failed GET from ${openidEndpoint}.`);
1023
+ }
1024
+ }
1025
+ }
1026
+ catch (error: any) {
1027
+ console.log("Failed to GET from openid well-known endpoint: ", error);
1028
+ }
1029
+ if (tenant.tid == "" || tenant.authority == "") {
1030
+ console.log(`GET from openid well-known endpoint failed to find tenant: ${response ? response.statusText : "unknown"}`);
1031
+ }
1032
+ return false; // failed, no need for UX to re-render
1033
+ }
925
1034
  //usersGet - GET from AAD Users endpoint
926
1035
  export async function usersGet(tenant: Tenant): Promise<{ users: string[], error: string }> {
927
1036
  // need a read or write access token to get graph users
@@ -970,26 +1079,77 @@ export async function configEdit(instance: IPublicClientApplication, authorizedU
970
1079
  export async function configRemove(instance: IPublicClientApplication, authorizedUser: User, config: Config, workspaceId: string, debug: boolean): Promise<APIResult> {
971
1080
  return configDelete(instance, authorizedUser, config, workspaceId, debug);
972
1081
  }
1082
+ export async function configsRefresh(instance: IPublicClientApplication, authorizedUser: User, workspaceId: string, ii: InitInfo, debug: boolean): Promise<APIResult> {
1083
+ let result: APIResult = new APIResult();
1084
+ if (debug) debugger;
1085
+ try {
1086
+ let workspace: Workspace = ii.ws.find((w) => w.id === workspaceId);
1087
+ if (workspace != null) {
1088
+ // clear Config associations as we are about to reset
1089
+ workspace.associatedConfigs.length = 0;
1090
+ // GET configs associated with this workspace
1091
+ let configsPromise: Promise<APIResult> = configsGet(instance, authorizedUser, workspace.id, debug);
1092
+ // wait for query to finish, return on any failure
1093
+ let [configsResult] = await Promise.all([configsPromise]);
1094
+ if (!configsResult.result) return configsResult;
1095
+ // process returned workspace components
1096
+ processReturnedConfigs(workspace, ii, configsResult.array!);
1097
+ // tag components with workspaceIDs
1098
+ ii.tagWithWorkspaces();
1099
+ }
1100
+ return result;
1101
+ }
1102
+ catch (error: any) {
1103
+ console.log(error.message);
1104
+ result.error = error.message;
1105
+ }
1106
+ result.result = false;
1107
+ result.status = 500;
1108
+ return result;
1109
+ }
973
1110
  // retrieve Workspace(s), User(s), Tenant(s), Config(s) given newly logged in user
974
1111
  export async function initGet(instance: IPublicClientApplication, authorizedUser: User, user: User, ii: InitInfo, tasks: TaskArray, debug: boolean): Promise<APIResult> {
975
1112
  let result: APIResult = new APIResult();
976
1113
  if (debug) debugger;
977
- // get tenant name and domain from AAD
978
- result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
979
- // if this is the first time, we have just gotten tenant info, then we must POST user and not-yet-onboarded tenant to back end
980
- if (result.result) {
981
- tasks.setTaskStart("POST config init", new Date());
982
- result = await initPost(instance, authorizedUser, user, debug);
983
- tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1114
+ // lookup authority for this user (the lookup call does it based on domain, but TID works as well to find authority)
1115
+ let tenant: Tenant = new Tenant();
1116
+ tenant.domain = user.tid;
1117
+ let bResult: boolean = await tenantUnauthenticatedLookup(tenant, debug);
1118
+ if (bResult) {
1119
+ // success, we at least got authority as a new bit of information at this point
1120
+ user.authority = tenant.authority;
1121
+ // do we have a logged in user from the same authority as this newly proposed tenant?
1122
+ let loggedInUser: User | undefined = ii.us.find((u: User) => (u.session === "Sign Out" && u.authority === user.authority));
1123
+ if (loggedInUser != null) {
1124
+ // get tenant name and domain from AAD
1125
+ result.result = await tenantRelationshipsGetById(user, ii, instance, tasks, debug);
1126
+ // if this is the first time, we have just gotten tenant info, then we must POST user and not-yet-onboarded tenant to back end
1127
+ if (result.result) {
1128
+ tasks.setTaskStart("POST config init", new Date());
1129
+ result = await initPost(instance, authorizedUser, user, debug);
1130
+ tasks.setTaskEnd("POST config init", new Date(), result.result ? "complete" : "failed");
1131
+ }
1132
+ // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
1133
+ if (result.result) {
1134
+ tasks.setTaskStart("GET workspaces", new Date());
1135
+ result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
1136
+ tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1137
+ }
1138
+ if (result.result) result.error = version;
1139
+ else console.log("@mindline/sync package version: " + version);
1140
+ return result;
1141
+ }
1142
+ else {
1143
+ result.error = `${user.mail} insufficient privileges to lookup under authority: ${user.authority}.`;
1144
+ result.result = false;
1145
+ return result;
1146
+ }
984
1147
  }
985
- // simlarly, if we just did our first post, then query config backend for workspace(s) associated with this user
986
- if (result.result) {
987
- tasks.setTaskStart("GET workspaces", new Date());
988
- result = await workspaceInfoGet(instance, authorizedUser, user, ii, debug);
989
- tasks.setTaskEnd("GET workspaces", new Date(), result.result ? "complete" : "failed");
1148
+ else {
1149
+ result.error = `Failed to retrieve authority for user "${user.mail}" TID ${user.tid}.`;
1150
+ result.result = false;
1151
+ return result;
990
1152
  }
991
- if (result.result) result.error = version;
992
- return result;
993
1153
  }
994
1154
  export async function tenantAdd(instance: IPublicClientApplication, authorizedUser: User, tenant: Tenant, workspaceId: string): Promise<APIResult> {
995
1155
  return tenantPost(instance, authorizedUser, tenant, workspaceId);
@@ -1053,7 +1213,7 @@ function processReturnedTenants(workspace: Workspace, ii: InitInfo, returnedTena
1053
1213
  // clear and overwrite dummy
1054
1214
  tenant = ii.ts.at(dummyIndex);
1055
1215
  } else {
1056
- // create and track new workspace
1216
+ // create and track new tenant
1057
1217
  tenant = new Tenant();
1058
1218
  ii.ts.push(tenant);
1059
1219
  }
@@ -1109,6 +1269,7 @@ function processReturnedConfigs(workspace: Workspace, ii: InitInfo, returnedConf
1109
1269
  tenantConfigInfo.sourceGroupName = tci.sourceGroupName;
1110
1270
  tenantConfigInfo.configurationTenantType = tci.configurationTenantType.toLowerCase();
1111
1271
  tenantConfigInfo.deltaToken = tci.deltaToken ?? "";
1272
+ tenantConfigInfo.configId = config!.id;
1112
1273
  config!.tenants.push(tenantConfigInfo);
1113
1274
  });
1114
1275
  // 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.35",
4
+ "version": "1.0.37",
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.",
@@ -13,9 +13,13 @@
13
13
  "author": "",
14
14
  "license": "ISC",
15
15
  "devDependencies": {
16
+ "@types/react": "^18.2.21",
17
+ "@types/react-dom": "^18.2.7",
18
+ "typescript": "^5.2.2",
16
19
  "vitest": "^0.29.8"
17
20
  },
18
21
  "dependencies": {
22
+ "@microsoft/signalr": "^7.0.10",
19
23
  "class-transformer": "^0.5.1",
20
24
  "reflect-metadata": "^0.1.13"
21
25
  }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": [ "DOM", "DOM.Iterable", "ESNext" ],
5
+ "types": [ "vite/client", "vite-plugin-svgr/client", "node", "jest" ],
6
+ "allowJs": false,
7
+ "skipLibCheck": false,
8
+ "esModuleInterop": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "node",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "composite": false,
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true
24
+ },
25
+ "include": ["src"],
26
+ "references": [{ "path": "./tsconfig.node.json" }]
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "node",
7
+ "allowSyntheticDefaultImports": true
8
+ }
9
+ }