@leapdev/app-platform 0.1.0-beta.3 → 0.1.0-beta.4

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.
Files changed (142) hide show
  1. package/.eslintrc.json +25 -0
  2. package/package.json +15 -15
  3. package/project.json +27 -0
  4. package/src/index.ts +44 -0
  5. package/src/lib/app-platform.ts +270 -0
  6. package/src/lib/environment/environment.dictionary.ts +28 -0
  7. package/src/lib/environment/environment.ts +44 -0
  8. package/src/lib/environment/{index.d.ts → index.ts} +2 -2
  9. package/src/lib/environment/leap/env.dev.au.config.ts +33 -0
  10. package/src/lib/environment/leap/env.live.au.config.ts +32 -0
  11. package/src/lib/environment/leap/env.live.ca.config.ts +32 -0
  12. package/src/lib/environment/leap/env.live.nz.config.ts +32 -0
  13. package/src/lib/environment/leap/env.live.uk.config.ts +32 -0
  14. package/src/lib/environment/leap/env.live.us.config.ts +32 -0
  15. package/src/lib/environment/leap/env.liveb.au.config.ts +32 -0
  16. package/src/lib/environment/leap/env.test.au.config.ts +33 -0
  17. package/src/lib/environment/leap/env.test.ca.config.ts +33 -0
  18. package/src/lib/environment/leap/env.test.nz.config.ts +33 -0
  19. package/src/lib/environment/leap/env.test.uk.config.ts +33 -0
  20. package/src/lib/environment/leap/env.test.us.config.ts +33 -0
  21. package/src/lib/functions/helpers.ts +100 -0
  22. package/src/lib/functions/xml.ts +41 -0
  23. package/src/lib/models/calc-api.model.ts +74 -0
  24. package/src/lib/models/constants/index.ts +29 -0
  25. package/src/lib/models/{index.d.ts → index.ts} +1 -1
  26. package/src/lib/models/leap-events.model.ts +79 -0
  27. package/src/lib/models/leap.model.ts +78 -0
  28. package/src/lib/models/signalr.model.ts +22 -0
  29. package/src/lib/services/auth/auth.service.ts +219 -0
  30. package/src/lib/services/calc-api/calc-api-service.interface.ts +12 -0
  31. package/src/lib/services/calc-api/calc-word.service.ts +117 -0
  32. package/src/lib/services/calc-api/promise-helper.function.ts +26 -0
  33. package/src/lib/services/custom-xml-part.service.ts +340 -0
  34. package/src/lib/services/document/document.service.ts +33 -0
  35. package/src/lib/services/{index.d.ts → index.ts} +10 -5
  36. package/src/lib/services/leap-events.service.ts +489 -0
  37. package/src/lib/services/logger/logger.service.ts +42 -0
  38. package/src/lib/services/signalr/signalr.service.ts +88 -0
  39. package/tsconfig.json +20 -0
  40. package/tsconfig.lib.json +12 -0
  41. package/src/index.d.ts +0 -25
  42. package/src/index.js +0 -42
  43. package/src/index.js.map +0 -1
  44. package/src/lib/app-platform.d.ts +0 -37
  45. package/src/lib/app-platform.js +0 -231
  46. package/src/lib/app-platform.js.map +0 -1
  47. package/src/lib/environment/environment.d.ts +0 -37
  48. package/src/lib/environment/environment.dictionary.d.ts +0 -4
  49. package/src/lib/environment/environment.dictionary.js +0 -30
  50. package/src/lib/environment/environment.dictionary.js.map +0 -1
  51. package/src/lib/environment/environment.js +0 -17
  52. package/src/lib/environment/environment.js.map +0 -1
  53. package/src/lib/environment/index.js +0 -9
  54. package/src/lib/environment/index.js.map +0 -1
  55. package/src/lib/environment/leap/env.dev.au.config.d.ts +0 -32
  56. package/src/lib/environment/leap/env.dev.au.config.js +0 -36
  57. package/src/lib/environment/leap/env.dev.au.config.js.map +0 -1
  58. package/src/lib/environment/leap/env.live.au.config.d.ts +0 -32
  59. package/src/lib/environment/leap/env.live.au.config.js +0 -36
  60. package/src/lib/environment/leap/env.live.au.config.js.map +0 -1
  61. package/src/lib/environment/leap/env.live.ca.config.d.ts +0 -32
  62. package/src/lib/environment/leap/env.live.ca.config.js +0 -36
  63. package/src/lib/environment/leap/env.live.ca.config.js.map +0 -1
  64. package/src/lib/environment/leap/env.live.nz.config.d.ts +0 -32
  65. package/src/lib/environment/leap/env.live.nz.config.js +0 -36
  66. package/src/lib/environment/leap/env.live.nz.config.js.map +0 -1
  67. package/src/lib/environment/leap/env.live.uk.config.d.ts +0 -32
  68. package/src/lib/environment/leap/env.live.uk.config.js +0 -36
  69. package/src/lib/environment/leap/env.live.uk.config.js.map +0 -1
  70. package/src/lib/environment/leap/env.live.us.config.d.ts +0 -32
  71. package/src/lib/environment/leap/env.live.us.config.js +0 -36
  72. package/src/lib/environment/leap/env.live.us.config.js.map +0 -1
  73. package/src/lib/environment/leap/env.liveb.au.config.d.ts +0 -32
  74. package/src/lib/environment/leap/env.liveb.au.config.js +0 -36
  75. package/src/lib/environment/leap/env.liveb.au.config.js.map +0 -1
  76. package/src/lib/environment/leap/env.test.au.config.d.ts +0 -32
  77. package/src/lib/environment/leap/env.test.au.config.js +0 -36
  78. package/src/lib/environment/leap/env.test.au.config.js.map +0 -1
  79. package/src/lib/environment/leap/env.test.ca.config.d.ts +0 -32
  80. package/src/lib/environment/leap/env.test.ca.config.js +0 -36
  81. package/src/lib/environment/leap/env.test.ca.config.js.map +0 -1
  82. package/src/lib/environment/leap/env.test.nz.config.d.ts +0 -32
  83. package/src/lib/environment/leap/env.test.nz.config.js +0 -36
  84. package/src/lib/environment/leap/env.test.nz.config.js.map +0 -1
  85. package/src/lib/environment/leap/env.test.uk.config.d.ts +0 -32
  86. package/src/lib/environment/leap/env.test.uk.config.js +0 -36
  87. package/src/lib/environment/leap/env.test.uk.config.js.map +0 -1
  88. package/src/lib/environment/leap/env.test.us.config.d.ts +0 -32
  89. package/src/lib/environment/leap/env.test.us.config.js +0 -36
  90. package/src/lib/environment/leap/env.test.us.config.js.map +0 -1
  91. package/src/lib/functions/helpers.d.ts +0 -5
  92. package/src/lib/functions/helpers.js +0 -95
  93. package/src/lib/functions/helpers.js.map +0 -1
  94. package/src/lib/functions/xml.d.ts +0 -2
  95. package/src/lib/functions/xml.js +0 -41
  96. package/src/lib/functions/xml.js.map +0 -1
  97. package/src/lib/models/calc-api.model.d.ts +0 -57
  98. package/src/lib/models/calc-api.model.js +0 -39
  99. package/src/lib/models/calc-api.model.js.map +0 -1
  100. package/src/lib/models/constants/index.d.ts +0 -28
  101. package/src/lib/models/constants/index.js +0 -32
  102. package/src/lib/models/constants/index.js.map +0 -1
  103. package/src/lib/models/index.js +0 -5
  104. package/src/lib/models/index.js.map +0 -1
  105. package/src/lib/models/leap-events.model.d.ts +0 -56
  106. package/src/lib/models/leap-events.model.js +0 -71
  107. package/src/lib/models/leap-events.model.js.map +0 -1
  108. package/src/lib/models/leap.model.d.ts +0 -63
  109. package/src/lib/models/leap.model.js +0 -29
  110. package/src/lib/models/leap.model.js.map +0 -1
  111. package/src/lib/models/signalr.model.d.ts +0 -21
  112. package/src/lib/models/signalr.model.js +0 -3
  113. package/src/lib/models/signalr.model.js.map +0 -1
  114. package/src/lib/services/auth/auth.service.d.ts +0 -13
  115. package/src/lib/services/auth/auth.service.js +0 -185
  116. package/src/lib/services/auth/auth.service.js.map +0 -1
  117. package/src/lib/services/calc-api/calc-api-service.interface.d.ts +0 -6
  118. package/src/lib/services/calc-api/calc-api-service.interface.js +0 -3
  119. package/src/lib/services/calc-api/calc-api-service.interface.js.map +0 -1
  120. package/src/lib/services/calc-api/calc-word.service.d.ts +0 -12
  121. package/src/lib/services/calc-api/calc-word.service.js +0 -99
  122. package/src/lib/services/calc-api/calc-word.service.js.map +0 -1
  123. package/src/lib/services/calc-api/promise-helper.function.d.ts +0 -2
  124. package/src/lib/services/calc-api/promise-helper.function.js +0 -32
  125. package/src/lib/services/calc-api/promise-helper.function.js.map +0 -1
  126. package/src/lib/services/custom-xml-part.service.d.ts +0 -5
  127. package/src/lib/services/custom-xml-part.service.js +0 -269
  128. package/src/lib/services/custom-xml-part.service.js.map +0 -1
  129. package/src/lib/services/document/document.service.d.ts +0 -16
  130. package/src/lib/services/document/document.service.js +0 -32
  131. package/src/lib/services/document/document.service.js.map +0 -1
  132. package/src/lib/services/index.js +0 -9
  133. package/src/lib/services/index.js.map +0 -1
  134. package/src/lib/services/leap-events.service.d.ts +0 -26
  135. package/src/lib/services/leap-events.service.js +0 -357
  136. package/src/lib/services/leap-events.service.js.map +0 -1
  137. package/src/lib/services/logger/logger.service.d.ts +0 -10
  138. package/src/lib/services/logger/logger.service.js +0 -39
  139. package/src/lib/services/logger/logger.service.js.map +0 -1
  140. package/src/lib/services/signalr/signalr.service.d.ts +0 -9
  141. package/src/lib/services/signalr/signalr.service.js +0 -93
  142. package/src/lib/services/signalr/signalr.service.js.map +0 -1
@@ -0,0 +1,219 @@
1
+ import { constants } from '../../models/constants';
2
+ import { LEAPUser } from '../../models/leap.model';
3
+
4
+ export class AuthService {
5
+ readonly #code: string;
6
+ readonly #codeVerifier: string;
7
+ readonly #clientId: string;
8
+ readonly #env: string;
9
+ readonly #authHost: string;
10
+
11
+ #user: LEAPUser | undefined;
12
+ #accessToken: string | undefined;
13
+ #refreshToken: string | undefined;
14
+
15
+ constructor(data: {
16
+ code: string;
17
+ codeVerifier;
18
+ clientId: string;
19
+ env: string;
20
+ }) {
21
+ this.#code = data.code;
22
+ this.#codeVerifier = data.codeVerifier;
23
+ this.#clientId = data.clientId;
24
+ this.#env = data.env.toLowerCase();
25
+ this.#authHost =
26
+ this.#env === 'live'
27
+ ? 'https://auth.leap.services'
28
+ : 'https://auth-test.leap.services';
29
+ }
30
+
31
+ async init(): Promise<boolean> {
32
+ try {
33
+ const data = await this.#exchangeAuthCodeForAccessToken();
34
+ this.#accessToken = data.access_token;
35
+ this.#refreshToken = data.refresh_token;
36
+ this.#user = await this.#getUserInfo();
37
+ return true;
38
+ } catch (e) {
39
+ console.log('[AuthService] unable to init');
40
+ return true;
41
+ }
42
+ }
43
+
44
+ async getRefreshedAccessToken(force?: boolean) {
45
+ if (this.#accessToken === undefined || !this.#accessToken) {
46
+ return '';
47
+ } else {
48
+ const decoded = this.#decodeAccessToken(this.#accessToken);
49
+
50
+ if (
51
+ force ||
52
+ (!!decoded &&
53
+ (decoded.exp - constants.tokenSecondsBeforeExpire) * 1000 <
54
+ Date.now())
55
+ ) {
56
+ const data = await this.#exchangeRefreshTokenForAccessToken();
57
+
58
+ if (data) {
59
+ this.#accessToken = data.access_token;
60
+ this.#refreshToken = data.refresh_token;
61
+ }
62
+ return this.#accessToken;
63
+ } else {
64
+ return this.#accessToken;
65
+ }
66
+ }
67
+ }
68
+
69
+ getLEAPUser() {
70
+ return this.#user;
71
+ }
72
+
73
+ async #exchangeAuthCodeForAccessToken(): Promise<{
74
+ access_token: string;
75
+ refresh_token: string;
76
+ }> {
77
+ return new Promise<{ access_token: string; refresh_token: string }>(
78
+ (resolve, reject) => {
79
+ this.#xhr<{ access_token: string; refresh_token: string }>(
80
+ {
81
+ method: 'POST',
82
+ endpoint: `${this.#authHost}/oauth/token`,
83
+ options: {
84
+ body: {
85
+ grant_type: 'authorization_code',
86
+ code: this.#code,
87
+ code_verifier: this.#codeVerifier,
88
+ client_id: this.#clientId,
89
+ redirect_uri: constants.fourdRedirectUrl,
90
+ },
91
+ },
92
+ accessToken: undefined,
93
+ },
94
+ (response, status) => {
95
+ if (status === 200) {
96
+ return resolve(response);
97
+ } else return reject(response);
98
+ }
99
+ );
100
+ }
101
+ );
102
+ }
103
+
104
+ async #getUserInfo(): Promise<LEAPUser> {
105
+ return new Promise<LEAPUser>((resolve, reject) => {
106
+ this.#xhr<LEAPUser>(
107
+ {
108
+ method: 'GET',
109
+ endpoint: `${this.#authHost}/api/userinfo`,
110
+ options: {},
111
+ accessToken: this.#accessToken,
112
+ },
113
+ (response, status) => {
114
+ if (status === 200) {
115
+ return resolve(response);
116
+ } else return reject(response);
117
+ }
118
+ );
119
+ });
120
+ }
121
+
122
+ async #exchangeRefreshTokenForAccessToken(): Promise<{
123
+ access_token: string;
124
+ refresh_token: string;
125
+ } | null> {
126
+ return new Promise<{ access_token: string; refresh_token: string } | null>(
127
+ (resolve, reject) => {
128
+ if (!this.#refreshToken) {
129
+ resolve(null);
130
+ } else {
131
+ this.#xhr<{ access_token: string; refresh_token: string }>(
132
+ {
133
+ method: 'POST',
134
+ endpoint: `${this.#authHost}/oauth/token`,
135
+ options: {
136
+ body: {
137
+ grant_type: 'refresh_token',
138
+ refresh_token: this.#refreshToken,
139
+ code_verifier: this.#codeVerifier,
140
+ client_id: this.#clientId,
141
+ },
142
+ },
143
+ accessToken: undefined,
144
+ },
145
+ (response, status) => {
146
+ if (status === 200) {
147
+ return resolve(response);
148
+ } else return reject(response);
149
+ }
150
+ );
151
+ }
152
+ }
153
+ );
154
+ }
155
+
156
+ #xhr<T>(
157
+ params: {
158
+ method: string;
159
+ endpoint: string;
160
+ options: any;
161
+ accessToken: string | undefined;
162
+ },
163
+ onload: (resp: T, status: number) => any
164
+ ): void {
165
+ const { method, endpoint, accessToken } = params;
166
+ let { options } = params;
167
+ options = {
168
+ ...{
169
+ contentType: 'application/json',
170
+ accessControlAllowOrigin: '*',
171
+ body: undefined,
172
+ },
173
+ ...options,
174
+ };
175
+
176
+ const xhr = new XMLHttpRequest();
177
+ xhr.open(method, endpoint, true);
178
+ xhr.setRequestHeader('Content-type', options.contentType);
179
+ xhr.setRequestHeader(
180
+ 'Access-Control-Allow-Origin',
181
+ options.accessControlAllowOrigin
182
+ );
183
+
184
+ if (accessToken)
185
+ xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
186
+
187
+ xhr.onload = () => {
188
+ try {
189
+ let resp;
190
+ if (xhr.response) resp = JSON.parse(xhr.response);
191
+ onload(resp, xhr.status);
192
+ } catch (e) {
193
+ onload(xhr.response, xhr.status);
194
+ }
195
+ };
196
+
197
+ if (options.body) options.body = JSON.stringify(options.body);
198
+
199
+ xhr.send(options.body);
200
+ }
201
+
202
+ #decodeAccessToken(accessToken: string | undefined) {
203
+ if (!accessToken) {
204
+ return undefined;
205
+ }
206
+
207
+ const payload = accessToken.split('.')[1];
208
+ if (payload) {
209
+ try {
210
+ const result = window.atob(payload);
211
+ return JSON.parse(result);
212
+ } catch (e) {
213
+ throw Error('Fail to decode access token.');
214
+ }
215
+ }
216
+
217
+ return undefined;
218
+ }
219
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ CreateSessionRequest,
3
+ EvaluatePathsRequest,
4
+ } from '../../models/calc-api.model';
5
+
6
+ export interface ICalcApiService {
7
+ createSession(request: CreateSessionRequest): Promise<string>;
8
+
9
+ destroySession(sessionId: string): Promise<void>;
10
+
11
+ evaluatePaths(request: EvaluatePathsRequest): Promise<Array<any> | null>;
12
+ }
@@ -0,0 +1,117 @@
1
+ import { v4 as uuid } from 'uuid';
2
+
3
+ import { ICalcApiService } from './calc-api-service.interface';
4
+ import { CreateSessionRequest, EvaluatePathsRequest, EvaluatePathsResponse } from '../../models/calc-api.model';
5
+ import { LeapEventsService } from '../leap-events.service';
6
+ import { LoggerService } from '../logger/logger.service';
7
+ import { LeapEvent, LeapEventAction, LeapEventType } from '../../models/leap-events.model';
8
+ import { timeout } from './promise-helper.function';
9
+
10
+ export class CalcWordService implements ICalcApiService {
11
+
12
+ constructor(
13
+ private leapEventsService: LeapEventsService,
14
+ private loggerService: LoggerService
15
+ ) { }
16
+
17
+ createSession(request: CreateSessionRequest): Promise<string> {
18
+ return new Promise((resolve, reject) => {
19
+ Promise.resolve(() => {
20
+ this.loggerService.log('[CalcApiOfflineService] (createSession) Creating CalcApi session');
21
+ })
22
+ .then(() => {
23
+ // TODO: Get Calc API sessionId from LEAP Desktop and return that
24
+ })
25
+ .then(() => {
26
+ this.loggerService.log('[CalcApiOfflineService] (createSession) LeapEvent CreateSession request sent');
27
+ resolve('');
28
+ })
29
+ .catch((error: any) => {
30
+ this.loggerService.error(`[CalcApiOfflineService] createSession() failed: ${error}`);
31
+ reject(error);
32
+ });
33
+ });
34
+ }
35
+
36
+ destroySession(sessionId: string): Promise<void> {
37
+ return new Promise((resolve, reject) => {
38
+ Promise.resolve(() => {
39
+ this.loggerService.log('[CalcApiOfflineService] (destroySession) Destroying CalcApi session');
40
+ })
41
+ .then(() => {
42
+ // TODO: Should we just fall through here?
43
+ })
44
+ .then(() => {
45
+ this.loggerService.log('[DocumentBuilderService] (destroySession) LeapEvent DestroySession request sent');
46
+ resolve();
47
+ })
48
+ .catch((error: any) => {
49
+ this.loggerService.error(`[DocumentBuilderService] destroySession() failed: ${error}`);
50
+ reject(error);
51
+ });
52
+ });
53
+ }
54
+
55
+ evaluatePaths(request: EvaluatePathsRequest): Promise<Array<EvaluatePathsResponse> | null> {
56
+ let subscriptionId: string;
57
+
58
+ // Check for empty matterFields - don't bother hitting the Local DocBuilder API in this case
59
+ if (request == null || (request.paths && request.paths.length === 0)) {
60
+ return Promise.resolve(null);
61
+ }
62
+
63
+ return new Promise((resolve, reject) => {
64
+ Promise.resolve(() => {
65
+ this.loggerService.log(
66
+ '[CalcApiWordService] (evaluatePaths) Evaluating paths on Local Calc API'
67
+ );
68
+ })
69
+ .then(() => {
70
+ const id = new Date().getTime().toString();
71
+ return this.leapEventsService.addEvent({
72
+ id,
73
+ correlationId: null,
74
+ eventType: LeapEventType.CalcApiRequest,
75
+ action: LeapEventAction.Calc_EvaluatePaths,
76
+ data: request
77
+ });
78
+ })
79
+ .then(event => {
80
+ this.loggerService.log('[CalcApiWordService] LeapEvent EvaluatePaths request sent');
81
+
82
+ // Subscribe
83
+ const leapEventResponseHandlerPromise = new Promise<Array<EvaluatePathsResponse>>((innerResolve, innerReject) => {
84
+ subscriptionId = this.leapEventsService.subscribe(
85
+ event.id,
86
+ LeapEventType.CalcApiResponse,
87
+ LeapEventAction.Calc_EvaluatePathsResponse,
88
+ (leapEvent: LeapEvent) => this.leapEventsService.handleLeapEventResponse<Array<EvaluatePathsResponse>>(
89
+ leapEvent, { resolve: innerResolve, reject: innerReject }
90
+ )
91
+ );
92
+ });
93
+
94
+ return timeout(leapEventResponseHandlerPromise, 5000);
95
+ })
96
+ .then((result) => {
97
+ // Return result
98
+ resolve(result);
99
+ })
100
+ .catch((error: any) => {
101
+ this.loggerService.error(`[CalcApiWordService] evaluatePaths() failed: ${error}`);
102
+ reject(error);
103
+ })
104
+ .then(() => {
105
+ // Unsubscribe
106
+ return this.leapEventsService.unsubscribe(subscriptionId);
107
+ })
108
+ .then((unsubscribeSuccess: boolean) => {
109
+ if (unsubscribeSuccess === true) {
110
+ this.loggerService.log(`[CalcApiWordService] (evaluatePaths) Successfully unsubscribed ${subscriptionId}`);
111
+ } else {
112
+ this.loggerService.warn(`[CalcApiWordService] (evaluatePaths) Failed to unsubscribe from ${subscriptionId}`);
113
+ }
114
+ });
115
+ });
116
+ }
117
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ Return a promise that resolves after duration milliseconds
3
+ */
4
+ export function sleep(duration: number) {
5
+ return new Promise<void>(function (resolve) {
6
+ setTimeout(() => {
7
+ resolve();
8
+ }, duration);
9
+ });
10
+ }
11
+
12
+ /*
13
+ Return a promise that rejects after duration milliseconds
14
+ */
15
+ export function timeout<T>(promise: Promise<T>, duration: number): Promise<T> {
16
+ if (duration === null || duration <= 0) {
17
+ return promise;
18
+ } else {
19
+ const timeoutPromise = new Promise<T>((resolve, reject) => {
20
+ setTimeout(() => {
21
+ reject(`Timed out after ${duration}ms`);
22
+ }, duration);
23
+ });
24
+ return Promise.race<T>([promise, timeoutPromise]);
25
+ }
26
+ }
@@ -0,0 +1,340 @@
1
+ /// <reference types="office-js" />
2
+
3
+ import { promisifyAsync } from '../functions/helpers';
4
+ import { toObj, toXml } from '../functions/xml';
5
+ const retryTimes = 5;
6
+
7
+ const getCustomXmlPartsByNamespace = (
8
+ namespace: string
9
+ ): Promise<Array<Office.CustomXmlPart> | null> => {
10
+ const officeDocument: Office.Document = Office.context.document;
11
+ if (officeDocument) {
12
+ const officeCustomXmlParts = officeDocument.customXmlParts;
13
+ //todo: use this format instead of promisifyAsync
14
+ return new Promise((resolve) => {
15
+ officeCustomXmlParts.getByNamespaceAsync(namespace, (result)=> {
16
+ resolve(result.value)
17
+ })
18
+ } )
19
+ } else {
20
+ return Promise.resolve(null);
21
+ }
22
+ };
23
+
24
+ //todo: remove all "any" reference in the project
25
+ const getCustomXmlPartByNamespace = (namespace: string): Promise<any> => {
26
+ return getCustomXmlPartsByNamespace(namespace)
27
+ .then((customXmlParts: any) => {
28
+ if (customXmlParts != null) {
29
+ // We have something
30
+ if (Array.isArray(customXmlParts)) {
31
+ if (customXmlParts.length > 0) {
32
+ return customXmlParts[0]; // Happy path
33
+ } else {
34
+ return null; // No error, but no matching custom XML part either
35
+ }
36
+ } else {
37
+ // We have something, but it's not an array
38
+ return customXmlParts;
39
+ }
40
+ }
41
+ return null; // No error, but no matching custom XML part either
42
+ })
43
+ .catch((error) => {
44
+ console.error(
45
+ error,
46
+ `[CustomXmlPartService] (getCustomXmlPartByNamespace) Failed to get custom XML part by namespace`
47
+ );
48
+ return null;
49
+ });
50
+ };
51
+
52
+ const getCustomXmlPartData = (partOrNamespace: string): Promise<any> => {
53
+ return new Promise((resolve, reject) => {
54
+ return new Promise((innerResolve) => {
55
+ if (typeof partOrNamespace === 'string') {
56
+ innerResolve(getCustomXmlPartByNamespace(partOrNamespace));
57
+ } else {
58
+ return innerResolve(partOrNamespace);
59
+ }
60
+ })
61
+ .then((customXmlPart: any) => {
62
+ if (customXmlPart != null) {
63
+ const getXmlAsync = promisifyAsync(
64
+ customXmlPart.getXmlAsync.bind(customXmlPart)
65
+ );
66
+ return getXmlAsync({});
67
+ } else {
68
+ return null;
69
+ }
70
+ })
71
+ .then((result) => {
72
+ resolve(toObj(result as string));
73
+ })
74
+ .catch((error) => {
75
+ console.error(
76
+ error,
77
+ `[CustomXmlPartService] (getCustomXmlPartData) Failed to get custom XML part data`
78
+ );
79
+ reject(error);
80
+ });
81
+ });
82
+ };
83
+
84
+ const getNodes = (
85
+ customXmlPart: Office.CustomXmlPart,
86
+ xPath: string = '/*'
87
+ ): Promise<Array<Office.CustomXmlNode>> => {
88
+ const getNodes = promisifyAsync<Array<Office.CustomXmlNode>>(
89
+ customXmlPart.getNodesAsync.bind(customXmlPart)
90
+ );
91
+ return getNodes(xPath);
92
+ };
93
+
94
+ const setNodeXml = (node: Office.CustomXmlNode, xml: string): Promise<void> => {
95
+ const setXml = promisifyAsync<void>(node.setXmlAsync.bind(node));
96
+ return setXml(xml, {});
97
+ };
98
+
99
+ const updateCustomXmlPart = (
100
+ customXmlPart: any,
101
+ xml: string,
102
+ rootTagName: string
103
+ ): Promise<void> => {
104
+ rootTagName = rootTagName || '';
105
+ const promises: Array<Promise<void>> = [];
106
+ // If custom XML part exists, replace it
107
+ return getNodes(customXmlPart)
108
+ .then((nodes: Array<Office.CustomXmlNode>) => {
109
+ for (let i = 0; i < nodes.length; i++) {
110
+ const node: any = nodes[i] as Office.CustomXmlNode;
111
+
112
+ if (
113
+ node &&
114
+ node.baseName &&
115
+ node.baseName.toUpperCase() === rootTagName.toUpperCase()
116
+ ) {
117
+ // Case-insensitive match
118
+ const promise = setNodeXml(node, xml)
119
+ .then(() => {
120
+ console.log(
121
+ '[CustomXmlPartService] updateCustomXmlPart succeeded'
122
+ );
123
+ })
124
+ .catch((error) => {
125
+ console.error(
126
+ error,
127
+ '[CustomXmlPartService] updateCustomXmlPart failed'
128
+ );
129
+ return Promise.reject(error);
130
+ });
131
+
132
+ promises.push(promise);
133
+ } else {
134
+ console.log(`Could not find ${rootTagName} in the custom XML part`);
135
+ }
136
+ }
137
+ })
138
+ .then(() => {
139
+ return Promise.all(promises);
140
+ })
141
+ .then(() => {
142
+ return Promise.resolve();
143
+ });
144
+ };
145
+
146
+ const addCustomXmlPart = (objOrXml: any, options?: any): Promise<void> => {
147
+ return new Promise<void>((resolve, reject) => {
148
+ Promise.resolve()
149
+ .then(() => {
150
+ // Setup values
151
+ let obj: any; //todo: check this obj
152
+ let xml: string;
153
+ options = options || { rootTagName: '' };
154
+ if (typeof objOrXml === 'string' || objOrXml instanceof String) {
155
+ obj = toObj(objOrXml as string);
156
+ xml = objOrXml as string;
157
+ } else {
158
+ obj = objOrXml;
159
+ xml = toXml(objOrXml, options.rootTagName, options);
160
+ }
161
+
162
+ const officeDocument: Office.Document = Office.context.document;
163
+ const officeCustomXmlParts = officeDocument.customXmlParts;
164
+ const add = promisifyAsync<Office.CustomXmlPart>(
165
+ officeCustomXmlParts.addAsync.bind(officeCustomXmlParts)
166
+ );
167
+ return add(xml, {});
168
+ })
169
+ .then((customXmlPart) => {
170
+ if (customXmlPart == null) {
171
+ // TODO
172
+ }
173
+ resolve();
174
+ })
175
+ .catch((error) => {
176
+ console.error(
177
+ error,
178
+ `[CustomXmlPartService] (addCustomXmlPart) Failed to add custom XML part`
179
+ );
180
+ reject(error);
181
+ });
182
+ });
183
+ };
184
+
185
+ const updateCustomXmlPartData = (
186
+ partOrNamespace: Office.CustomXmlPart | string,
187
+ objOrXml: any,
188
+ options?: any
189
+ ): Promise<boolean> => {
190
+ return new Promise<boolean>((resolve, reject) => {
191
+ return new Promise<Office.CustomXmlPart | null>((innerResolve) => {
192
+ if (typeof partOrNamespace === 'string' || partOrNamespace instanceof String) {
193
+ innerResolve(getCustomXmlPartByNamespace(partOrNamespace as string));
194
+ } else {
195
+ return innerResolve(partOrNamespace);
196
+ }
197
+ })
198
+ .then((customXmlPart) => {
199
+ // Setup values
200
+ let obj: any;
201
+ let xml: string;
202
+ options = options || { rootTagName: '' };
203
+ if (typeof objOrXml === 'string' || objOrXml instanceof String) {
204
+ obj = toObj(objOrXml as string);
205
+ xml = objOrXml as string;
206
+ } else {
207
+ obj = objOrXml;
208
+ xml = toXml(objOrXml, options.rootTagName, options);
209
+ }
210
+
211
+ if (customXmlPart != null) {
212
+ return updateCustomXmlPart(customXmlPart, xml, options.rootTagName);
213
+ } else {
214
+ return addCustomXmlPart(xml, options);
215
+ }
216
+ })
217
+ .then(() => {
218
+ resolve(true);
219
+ })
220
+ .catch((error) => {
221
+ console.error(
222
+ error,
223
+ `[CustomXmlPartService] (updateCustomXmlPartData) Failed to update ` +
224
+ `${
225
+ typeof partOrNamespace === 'string' ? partOrNamespace : ''
226
+ } custom XML part`
227
+ );
228
+ reject(error);
229
+ });
230
+ });
231
+ };
232
+
233
+ const retryUpdateCustomXmlPartData = (
234
+ namespace: string,
235
+ data: string,
236
+ options: { rootTagName: string } & { [key: string]: any },
237
+ retries = retryTimes
238
+ ): Promise<void> => {
239
+ const { rootTagName } = options;
240
+ return new Promise<void>((resolve, reject) => {
241
+ console.log(
242
+ `[CustomXmlPartService] Updating ${namespace} tag ${rootTagName}`
243
+ );
244
+ updateCustomXmlPartData(namespace, data, options)
245
+ .then(() => {
246
+ resolve();
247
+ })
248
+ .catch((error) => {
249
+ if (retries > 0) {
250
+ console.log(
251
+ `[CustomXmlPartService] Update ${namespace} tag ${rootTagName} failed - Retrying ${
252
+ retryTimes - retries + 1
253
+ }...`
254
+ );
255
+ setTimeout(() => {
256
+ retryUpdateCustomXmlPartData(namespace, data, options, retries - 1)
257
+ .then(resolve)
258
+ .catch(reject);
259
+ }, 2000);
260
+ } else {
261
+ reject(error);
262
+ }
263
+ });
264
+ });
265
+ };
266
+
267
+ export const getDataByNamespace = (
268
+ namespace: string,
269
+ retries = 6
270
+ ): Promise<any> => {
271
+ return new Promise((resolve, reject) => {
272
+ getCustomXmlPartData(namespace)
273
+ .then((data) => {
274
+ resolve(data);
275
+ })
276
+ .catch((_) => {
277
+ if (retries > 0) {
278
+ console.log(
279
+ `[CustomXmlPartService] Get data from ${namespace} failed - Retrying ${
280
+ retryTimes - retries + 1
281
+ }...`
282
+ );
283
+ setTimeout(() => {
284
+ getDataByNamespace(namespace, retries - 1)
285
+ .then(resolve)
286
+ .catch(reject);
287
+ }, 2000);
288
+ } else {
289
+ reject(`Unable to get custom XML part using namespace ${namespace}`);
290
+ }
291
+ });
292
+ });
293
+ };
294
+
295
+ export const updateXmlDataByNamespace = (
296
+ namespace: string,
297
+ data: string,
298
+ rootTagName: string
299
+ ): Promise<void> => {
300
+ const options = { rootTagName };
301
+ return retryUpdateCustomXmlPartData(namespace, data, options);
302
+ };
303
+
304
+ export const addHandlerForCustomXmlPart = (
305
+ namespace: string,
306
+ eventType: Office.EventType,
307
+ handler: any
308
+ ): Promise<void | null> => {
309
+ return getCustomXmlPartByNamespace(namespace).then((customXmlPart: any) => {
310
+ if (customXmlPart != null) {
311
+ const addHandler = promisifyAsync<void>(
312
+ customXmlPart.addHandlerAsync.bind(customXmlPart)
313
+ );
314
+ return addHandler(eventType, handler, {});
315
+ } else {
316
+ return null;
317
+ }
318
+ });
319
+ };
320
+
321
+ export const getChildNodes = (
322
+ node: Office.CustomXmlNode,
323
+ xPath: string = '*'
324
+ ): Promise<Array<Office.CustomXmlNode>> => {
325
+ const getNodes = promisifyAsync<Array<Office.CustomXmlNode>>(
326
+ node.getNodesAsync.bind(node)
327
+ );
328
+ return getNodes(xPath);
329
+ };
330
+
331
+ export const getNodeText = (
332
+ node: Office.CustomXmlNode | null
333
+ ): Promise<string> => {
334
+ if (node) {
335
+ const getText = promisifyAsync<string>(node.getTextAsync.bind(node));
336
+ return getText({});
337
+ } else {
338
+ return Promise.resolve('');
339
+ }
340
+ };