@esri/solution-deployer 4.1.2 → 5.0.0

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 (41) hide show
  1. package/dist/cjs/deploySolutionFromTemplate.d.ts +48 -48
  2. package/dist/cjs/deploySolutionFromTemplate.js +331 -331
  3. package/dist/cjs/deploySolutionFromTemplate.js.map +1 -1
  4. package/dist/cjs/deploySolutionItems.d.ts +224 -224
  5. package/dist/cjs/deploySolutionItems.js +853 -849
  6. package/dist/cjs/deploySolutionItems.js.map +1 -1
  7. package/dist/cjs/deployer.d.ts +34 -34
  8. package/dist/cjs/deployer.js +101 -101
  9. package/dist/cjs/deployerUtils.d.ts +47 -47
  10. package/dist/cjs/deployerUtils.js +123 -123
  11. package/dist/cjs/helpers/post-process.d.ts +29 -29
  12. package/dist/cjs/helpers/post-process.js +61 -61
  13. package/dist/cjs/helpers/share-templates-to-groups.d.ts +24 -24
  14. package/dist/cjs/helpers/share-templates-to-groups.js +64 -64
  15. package/dist/cjs/helpers/sortTemplates.d.ts +23 -23
  16. package/dist/cjs/helpers/sortTemplates.js +14 -14
  17. package/dist/cjs/index.d.ts +24 -24
  18. package/dist/cjs/index.js +27 -27
  19. package/dist/cjs/module-map.d.ts +23 -23
  20. package/dist/cjs/module-map.js +195 -195
  21. package/dist/esm/deploySolutionFromTemplate.d.ts +48 -48
  22. package/dist/esm/deploySolutionFromTemplate.js +317 -317
  23. package/dist/esm/deploySolutionFromTemplate.js.map +1 -1
  24. package/dist/esm/deploySolutionItems.d.ts +224 -224
  25. package/dist/esm/deploySolutionItems.js +830 -826
  26. package/dist/esm/deploySolutionItems.js.map +1 -1
  27. package/dist/esm/deployer.d.ts +34 -34
  28. package/dist/esm/deployer.js +96 -96
  29. package/dist/esm/deployerUtils.d.ts +47 -47
  30. package/dist/esm/deployerUtils.js +115 -115
  31. package/dist/esm/helpers/post-process.d.ts +29 -29
  32. package/dist/esm/helpers/post-process.js +57 -57
  33. package/dist/esm/helpers/share-templates-to-groups.d.ts +24 -24
  34. package/dist/esm/helpers/share-templates-to-groups.js +60 -60
  35. package/dist/esm/helpers/sortTemplates.d.ts +23 -23
  36. package/dist/esm/helpers/sortTemplates.js +10 -10
  37. package/dist/esm/index.d.ts +24 -24
  38. package/dist/esm/index.js +24 -24
  39. package/dist/esm/module-map.d.ts +23 -23
  40. package/dist/esm/module-map.js +191 -191
  41. package/package.json +12 -12
@@ -1,827 +1,831 @@
1
- /** @license
2
- * Copyright 2018 Esri
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- /**
17
- * Manages deployment of items via the REST API.
18
- *
19
- * @module deployItems
20
- */
21
- import * as common from "@esri/solution-common";
22
- import { moduleMap } from "./module-map";
23
- const UNSUPPORTED = null;
24
- // ------------------------------------------------------------------------------------------------------------------ //
25
- /**
26
- * Deploys a set of items defined by templates.
27
- *
28
- * @param portalSharingUrl Server/sharing
29
- * @param storageItemId Id of storage item
30
- * @param templates A collection of AGO item templates
31
- * @param storageAuthentication Credentials for the organization with the source items
32
- * @param templateDictionary Hash of facts: org URL, adlib replacements
33
- * @param deployedSolutionId Id of deployed Solution item
34
- * @param destinationAuthentication Credentials for the destination organization
35
- * @param options Options to tune deployment
36
- * @returns A promise that will resolve with the list of information about the created items
37
- */
38
- export function deploySolutionItems(portalSharingUrl, storageItemId, templates, storageAuthentication, templateDictionary, deployedSolutionId, destinationAuthentication, options) {
39
- return new Promise((resolve, reject) => {
40
- // Prepare feedback mechanism
41
- const totalEstimatedCost = _estimateDeploymentCost(templates) + 1; // solution items, plus avoid divide by 0
42
- let percentDone = 10; // allow for previous deployment work
43
- const progressPercentStep = (99 - percentDone) / totalEstimatedCost; // leave some % for caller for wrapup
44
- const failedTemplateItemIds = [];
45
- const deployedItemIds = [];
46
- let statusOK = true;
47
- // TODO: move to separate fn
48
- const itemProgressCallback = (itemId, status, costUsed, createdItemId // supplied when status is EItemProgressStatus.Created or .Finished
49
- ) => {
50
- percentDone += progressPercentStep * costUsed;
51
- /* istanbul ignore else */
52
- if (options.progressCallback) {
53
- if (status === common.EItemProgressStatus.Finished) {
54
- const event = {
55
- event: common.SItemProgressStatus[status],
56
- data: itemId
57
- };
58
- options.progressCallback(Math.round(percentDone), options.jobId, event);
59
- }
60
- else {
61
- options.progressCallback(Math.round(percentDone), options.jobId);
62
- }
63
- }
64
- /* istanbul ignore if */
65
- if (options.consoleProgress) {
66
- console.log(Date.now(), itemId, options.jobId ?? "", common.SItemProgressStatus[status], percentDone.toFixed(0) + "%", costUsed, createdItemId ? "==> " + createdItemId : "");
67
- }
68
- if (status === common.EItemProgressStatus.Created) {
69
- deployedItemIds.push(createdItemId);
70
- }
71
- else if (status === common.EItemProgressStatus.Failed) {
72
- failedTemplateItemIds.push(itemId);
73
- console.error("Item " + itemId + " has failed");
74
- statusOK = false;
75
- }
76
- return statusOK;
77
- // ---------------------------------------------------------------------------------------------------------------
78
- };
79
- // portal does not allow views of a single source to be created at the same time
80
- if (common.getProp(templateDictionary, "organization.isPortal")) {
81
- templates = _evaluateSharedViewSources(templates);
82
- }
83
- // Create an ordered graph of the templates so that dependencies are created before the items that need them.
84
- // Because cycles are permitted, we also keep track of items that need to be patched later because their
85
- // dependencies are necessarily created after they are created.
86
- const { buildOrder, itemsToBePatched } = common.topologicallySortItems(templates);
87
- // For each item in order from no dependencies to dependent on other items,
88
- // * replace template symbols using template dictionary
89
- // * create item in destination group
90
- // * add created item's id into the template dictionary
91
- const awaitAllItems = [];
92
- const reuseItemsDef = _reuseDeployedItems(templates, options.enableItemReuse ?? false, templateDictionary, destinationAuthentication);
93
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
94
- reuseItemsDef.then(() => {
95
- const useExistingItemsDef = _useExistingItems(templates, common.getProp(templateDictionary, "params.useExisting"), templateDictionary, destinationAuthentication);
96
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
97
- useExistingItemsDef.then(() => {
98
- templates = common.setNamesAndTitles(templates, templateDictionary.solutionItemId);
99
- buildOrder.forEach((id) => {
100
- // Get the item's template out of the list of templates
101
- const template = common.findTemplateInList(templates, id);
102
- awaitAllItems.push(_createItemFromTemplateWhenReady(template, common.generateStorageFilePaths(portalSharingUrl, storageItemId, template.resources, options.storageVersion), storageAuthentication, templateDictionary, destinationAuthentication, itemProgressCallback));
103
- });
104
- // Wait until all items have been created
105
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
106
- Promise.all(awaitAllItems).then((clonedSolutionItems) => {
107
- if (failedTemplateItemIds.length === 0) {
108
- // Do we have any items to be patched (i.e., they refer to dependencies using the template id rather
109
- // than the cloned id because the item had to be created before the dependency)? Flag these items
110
- // for post processing in the list of clones.
111
- _flagPatchItemsForPostProcessing(itemsToBePatched, templateDictionary, clonedSolutionItems);
112
- resolve(clonedSolutionItems);
113
- }
114
- else {
115
- // Delete created items
116
- const progressOptions = {
117
- consoleProgress: true
118
- };
119
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
120
- common
121
- .deleteSolutionByComponents(deployedSolutionId, deployedItemIds, templates, templateDictionary, destinationAuthentication, progressOptions)
122
- .then(() => reject(common.failWithIds(failedTemplateItemIds)));
123
- }
124
- });
125
- });
126
- }, e => {
127
- console.error(e);
128
- reject(common.fail(e));
129
- });
130
- });
131
- }
132
- /**
133
- * For each item to be patched, convert it to its cloned id and mark the item as needing post processing.
134
- *
135
- * @param itemsToBePatched List of items that need to have their dependencies patched
136
- * @param templateDictionary Hash of facts: org URL, adlib replacements
137
- * @param templates A collection of AGO item templates
138
- * @private
139
- */
140
- export function _flagPatchItemsForPostProcessing(itemsToBePatched, templateDictionary, templates) {
141
- let itemIdsToBePatched = Object.keys(itemsToBePatched);
142
- /* istanbul ignore else */
143
- if (itemIdsToBePatched.length > 0) {
144
- // Replace the ids of the items to be patched (which are template ids) with their cloned versions
145
- itemIdsToBePatched = itemIdsToBePatched.map(id => templateDictionary[id].itemId);
146
- // Make sure that the items to be patched are flagged for post processing
147
- templates.forEach(item => {
148
- /* istanbul ignore else */
149
- if (itemIdsToBePatched.includes(item.id)) {
150
- item.postProcess = true;
151
- }
152
- });
153
- }
154
- }
155
- /**
156
- * Portal does not allow views of a single source to be created at the same time.
157
- *
158
- * Update view templates with an array of other view template ids that it should wait on.
159
- *
160
- * @param templates a collection of AGO item templates
161
- *
162
- * @returns An updated array of item templates
163
- * @private
164
- */
165
- export function _evaluateSharedViewSources(templates) {
166
- // update the templates so we can defer the deployment when more than one view shares the same source
167
- // these are not classic dependencies but are in some ways similar
168
- const views = _getViews(templates);
169
- _updateViewTemplates(templates, views);
170
- const viewHash = _getViewHash(views);
171
- let processed = [];
172
- const visited = [];
173
- Object.keys(viewHash).forEach(k => {
174
- const _views = viewHash[k];
175
- _views.forEach(cv => {
176
- const template = common.findTemplateInList(templates, cv);
177
- const syncViews = common.getProp(template, "properties.syncViews");
178
- /* istanbul ignore else */
179
- if (visited.indexOf(template.itemId) > -1) {
180
- processed = processed.concat(syncViews);
181
- }
182
- /* istanbul ignore else */
183
- if (syncViews && syncViews.length > 0) {
184
- // when a view has multiple dependencies we need to retain the syncViews if they have been set already...
185
- common.setProp(template, "properties.syncViews", common.cloneObject(processed));
186
- }
187
- /* istanbul ignore else */
188
- if (processed.indexOf(cv) < 0) {
189
- processed.push(cv);
190
- }
191
- /* istanbul ignore else */
192
- if (visited.indexOf(template.itemId) < 0) {
193
- visited.push(template.itemId);
194
- }
195
- });
196
- processed = [];
197
- });
198
- return templates;
199
- }
200
- /**
201
- * Add a syncViews array to each template that will hold all other view ids that
202
- * have the same FS dependency.
203
- * These arrays will be processed later to only contain ids that each view will need to wait on.
204
- *
205
- * @param templates a collection of AGO item templates
206
- * @param views an array of view template details
207
- *
208
- * @returns An updated array of item templates
209
- * @private
210
- */
211
- export function _updateViewTemplates(templates, views) {
212
- views.forEach(v => {
213
- v.dependencies.forEach((id) => {
214
- templates = templates.map(t => {
215
- /* istanbul ignore else */
216
- if (common.getProp(t, "properties.service.isView") &&
217
- t.dependencies.indexOf(id) > -1 &&
218
- t.itemId !== v.id) {
219
- /* istanbul ignore else */
220
- if (!Array.isArray(t.properties.syncViews)) {
221
- t.properties.syncViews = [];
222
- }
223
- /* istanbul ignore else */
224
- if (t.properties.syncViews.indexOf(v.id) < 0) {
225
- t.properties.syncViews.push(v.id);
226
- }
227
- }
228
- return t;
229
- });
230
- });
231
- });
232
- return templates;
233
- }
234
- /**
235
- * Get all view templates from the source templates collection
236
- *
237
- * @param views A collection of view ID and dependencies
238
- *
239
- * @returns an array of objects with the source FS id as the key and a list of views that are
240
- * dependant upon it
241
- *
242
- * @private
243
- */
244
- export function _getViewHash(views) {
245
- const viewHash = {};
246
- views.forEach(v => {
247
- v.dependencies.forEach((d) => {
248
- /* istanbul ignore else */
249
- if (Object.keys(viewHash).indexOf(d) < 0) {
250
- viewHash[d] = [v.id];
251
- }
252
- else if (viewHash[d].indexOf(v.id) < 0) {
253
- viewHash[d].push(v.id);
254
- }
255
- });
256
- });
257
- return viewHash;
258
- }
259
- /**
260
- * Get all view templates from the source templates collection
261
- *
262
- * @param templates A collection of AGO item templates
263
- *
264
- * @returns an array with the view id and its dependencies
265
- *
266
- * @private
267
- */
268
- export function _getViews(templates) {
269
- return templates.reduce((acc, v) => {
270
- /* istanbul ignore else */
271
- if (common.getProp(v, "properties.service.isView")) {
272
- acc.push({
273
- id: v.itemId,
274
- dependencies: v.dependencies
275
- });
276
- }
277
- return acc;
278
- }, []);
279
- }
280
- /**
281
- * Search for existing items and update the templateDictionary with key details
282
- *
283
- * @param templates A collection of AGO item templates
284
- * @param reuseItems Option to search for existing items
285
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
286
- * @param authentication Credentials for the requests
287
- *
288
- * @returns A Promise that will resolve once existing items have been evaluated
289
- *
290
- * @private
291
- */
292
- export function _reuseDeployedItems(templates, reuseItems, templateDictionary, authentication) {
293
- return new Promise((resolve, reject) => {
294
- if (reuseItems) {
295
- const findItemsByKeywordResponse = _findExistingItemByKeyword(templates, templateDictionary, authentication);
296
- const existingItemsByKeyword = findItemsByKeywordResponse.existingItemsDefs;
297
- const existingItemInfos = findItemsByKeywordResponse.existingItemInfos;
298
- Promise.all(existingItemsByKeyword).then((existingItemsByKeywordResponse) => {
299
- const findExistingItemsByTag = _handleExistingItems(existingItemsByKeywordResponse, existingItemInfos, templateDictionary, authentication, true);
300
- const existingItemsByTag = findExistingItemsByTag.existingItemsDefs;
301
- const existingItemInfosByTag = findExistingItemsByTag.existingItemInfos;
302
- Promise.all(existingItemsByTag).then(existingItemsByTagResponse => {
303
- _handleExistingItems(existingItemsByTagResponse, existingItemInfosByTag, templateDictionary, authentication, false);
304
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
305
- _updateTemplateDictionary(templates, templateDictionary, authentication).then(resolve);
306
- }, e => reject(common.fail(e)));
307
- }, e => reject(common.fail(e)));
308
- }
309
- else {
310
- resolve(null);
311
- }
312
- });
313
- }
314
- /**
315
- * Search for existing items and update the templateDictionary with key details
316
- *
317
- * Subtle difference between _reuseDeployedItems and _useExistingItems
318
- * _reuseDeployedItems: will search all existing items based on specific type keywords
319
- * that would have been added by a previous deployment
320
- * _useExistingItems: will search for an existing item that the user provided
321
- * the item id for while configuring in the deployment app.
322
- * This type of item would not necessarily have been laid down by a previous deployment and
323
- * can thus not expect that it will have the type keywords
324
- *
325
- * @param templates A collection of AGO item templates
326
- * @param useExisting Option to search for existing items
327
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
328
- * @param authentication Credentials for the requests
329
- *
330
- * @returns A Promise that will resolve once existing items have been evaluated
331
- *
332
- * @private
333
- */
334
- export function _useExistingItems(templates, useExisting, templateDictionary, authentication) {
335
- return new Promise(resolve => {
336
- if (useExisting) {
337
- const itemDefs = [];
338
- const sourceIdHash = {};
339
- const itemIds = [];
340
- Object.keys(templateDictionary.params).forEach(k => {
341
- const v = templateDictionary.params[k];
342
- /* istanbul ignore else */
343
- if (v.itemId && /[0-9A-F]{32}/i.test(k)) {
344
- _updateTemplateDictionaryById(templateDictionary, k, v.itemId, v);
345
- // need to check and set the typeKeyword if it doesn't exist on this service yet
346
- // when the user has passed in an itemId that does not come from a previous deployment
347
- itemDefs.push(common.getItemBase(v.itemId, authentication));
348
- sourceIdHash[v.itemId] = k;
349
- /* istanbul ignore else */
350
- if (itemIds.indexOf(k) < 0) {
351
- itemIds.push(k);
352
- }
353
- }
354
- });
355
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
356
- _setTypekeywordForExisting(itemDefs, sourceIdHash, authentication).then(() => {
357
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
358
- _updateTemplateDictionary(itemIds.map(id => common.getTemplateById(templates, id)), templateDictionary, authentication).then(resolve);
359
- });
360
- }
361
- else {
362
- resolve(null);
363
- }
364
- });
365
- }
366
- /**
367
- * Verify if the existing item has the source-<itemId> typeKeyword and set it if not
368
- * This allows items that did not come from deployment to be found for reuse after they
369
- * have been used once via a custom itemId param
370
- *
371
- * @param itemDefs
372
- * @param sourceIdHash key value pairs..actual itemId is the key and the source itemId is the value
373
- * @param authentication credentials for the requests
374
- *
375
- * @returns a promise to indicate when the requests are complete
376
- * @private
377
- */
378
- export function _setTypekeywordForExisting(itemDefs, sourceIdHash, authentication) {
379
- return new Promise(resolve => {
380
- if (itemDefs.length > 0) {
381
- Promise.all(itemDefs).then(results => {
382
- const itemUpdateDefs = [];
383
- results.forEach(result => {
384
- const sourceId = sourceIdHash[result.id];
385
- /* istanbul ignore else */
386
- if (result && sourceId && result.typeKeywords) {
387
- const sourceKeyword = `source-${sourceId}`;
388
- const typeKeywords = result.typeKeywords;
389
- /* istanbul ignore else */
390
- if (typeKeywords.indexOf(sourceKeyword) < 0) {
391
- typeKeywords.push(sourceKeyword);
392
- const itemUpdate = { id: result.id, typeKeywords };
393
- itemUpdateDefs.push(common.updateItem(itemUpdate, authentication));
394
- }
395
- }
396
- });
397
- // wait for updates to finish before we resolve
398
- if (itemUpdateDefs.length > 0) {
399
- Promise.all(itemUpdateDefs).then(resolve, () => resolve(undefined));
400
- }
401
- else {
402
- resolve(undefined);
403
- }
404
- }, () => resolve(undefined));
405
- }
406
- else {
407
- resolve(undefined);
408
- }
409
- });
410
- }
411
- /**
412
- * Update the templateDictionary with key details by item type
413
- *
414
- * @param templates A collection of AGO item templates
415
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
416
- *
417
- * @private
418
- */
419
- export function _updateTemplateDictionary(templates, templateDictionary, authentication) {
420
- return new Promise(resolve => {
421
- const defs = [];
422
- const urls = [];
423
- const types = [];
424
- const ids = [];
425
- templates.forEach(t => {
426
- const templateInfo = templateDictionary[t.itemId];
427
- /* istanbul ignore else */
428
- if (templateInfo && templateInfo.url && templateInfo.itemId) {
429
- /* istanbul ignore else */
430
- if (t.item.type === "Feature Service") {
431
- const enterpriseIDMapping = common.getProp(templateDictionary, `params.${t.itemId}.enterpriseIDMapping`);
432
- Object.assign(templateDictionary[t.itemId], common.getLayerSettings(common.getLayersAndTables(t), templateInfo.url, templateInfo.itemId, enterpriseIDMapping));
433
- // if the service has veiws keep track of the fields so we can use them to
434
- // compare with the view fields
435
- /* istanbul ignore else */
436
- if (common.getProp(t, "properties.service.hasViews")) {
437
- common._updateTemplateDictionaryFields(t, templateDictionary, false);
438
- }
439
- }
440
- // for fs query with its url...for non fs query the item
441
- // this is to verify situations where we have a stale search index that will
442
- // say some items exist when they don't really exist
443
- // searching the services url or with the item id will return an error when this condition occurs
444
- /* istanbul ignore else */
445
- if (urls.indexOf(templateInfo.url) < 0) {
446
- defs.push(t.item.type === "Feature Service"
447
- ? common.rest_request(templateInfo.url, { authentication })
448
- : common.getItemBase(templateInfo.itemId, authentication));
449
- urls.push(templateInfo.url);
450
- types.push(t.item.type);
451
- ids.push(templateInfo.itemId);
452
- }
453
- }
454
- });
455
- if (defs.length > 0) {
456
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
457
- Promise.all(defs.map(p => p.catch(e => e))).then(results => {
458
- /* istanbul ignore else */
459
- if (Array.isArray(results) && results.length > 0) {
460
- const fieldDefs = [];
461
- results.forEach((r, i) => {
462
- // a feature service result will contain a serviceItemId if it was successfully fetched
463
- if (r.serviceItemId && types[i] === "Feature Service") {
464
- Object.keys(templateDictionary).forEach(k => {
465
- const v = templateDictionary[k];
466
- /* istanbul ignore else */
467
- if (v.itemId && v.itemId === r.serviceItemId) {
468
- common.setDefaultSpatialReference(templateDictionary, k, r.spatialReference);
469
- // keep the extent values from these responses as well
470
- common.setCreateProp(templateDictionary, `${k}.defaultExtent`, r.fullExtent || r.initialExtent);
471
- const layerIds = (r.layers || []).map((l) => l.id);
472
- const tablesIds = (r.tables || []).map((t) => t.id);
473
- fieldDefs.push(common.getExistingLayersAndTables(urls[i], layerIds.concat(tablesIds), authentication));
474
- }
475
- });
476
- }
477
- else {
478
- /* istanbul ignore else */
479
- if (types[i] === "Feature Service" ||
480
- common.getProp(r, "response.error")) {
481
- // if an error is returned we need to clean up the templateDictionary
482
- templateDictionary = _updateTemplateDictionaryForError(templateDictionary, ids[i]);
483
- }
484
- }
485
- });
486
- if (fieldDefs.length > 0) {
487
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
488
- Promise.all(fieldDefs).then(layerTableResult => {
489
- layerTableResult.forEach(l => {
490
- l.forEach((ll) => {
491
- Object.keys(templateDictionary).forEach(k => {
492
- /* istanbul ignore else */
493
- if (templateDictionary[k].itemId === ll.serviceItemId) {
494
- let sourceId = "";
495
- Object.keys(templateDictionary).some(_k => {
496
- /* istanbul ignore else */
497
- if (templateDictionary[_k].itemId === ll.serviceItemId) {
498
- sourceId = _k;
499
- return true;
500
- }
501
- });
502
- const enterpriseIDMapping = common.getProp(templateDictionary, `params.${sourceId}.enterpriseIDMapping`);
503
- if (enterpriseIDMapping) {
504
- Object.keys(enterpriseIDMapping).forEach(id => {
505
- if (enterpriseIDMapping[id].toString() ===
506
- ll.id.toString()) {
507
- _setFields(templateDictionary, k, id, ll.fields);
508
- }
509
- });
510
- }
511
- else {
512
- _setFields(templateDictionary, k, ll.id, ll.fields);
513
- }
514
- }
515
- });
516
- });
517
- });
518
- resolve(null);
519
- });
520
- }
521
- else {
522
- resolve(null);
523
- }
524
- }
525
- else {
526
- resolve(null);
527
- }
528
- });
529
- }
530
- else {
531
- resolve(null);
532
- }
533
- });
534
- }
535
- /**
536
- * Add the fields from the source layer to the template dictionary for any required replacements
537
- *
538
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
539
- * @param itemId the id for the item
540
- * @param layerId the id for the layer
541
- * @param fields the fields to transfer
542
- *
543
- * @private
544
- */
545
- export function _setFields(templateDictionary, itemId, layerId, fields) {
546
- const layerInfo = common.getProp(templateDictionary, `${itemId}.layer${layerId}`);
547
- /* istanbul ignore else */
548
- if (layerInfo && fields) {
549
- layerInfo.fields = fields;
550
- }
551
- }
552
- /**
553
- * In some cases an item id search will return a stale item reference
554
- * it will subsequently fail when we try to fetch the underlying service.
555
- *
556
- * We need to remove the item info that has been added to the template dictionary
557
- * and treat the item as we do other items that don't already exist on deployment.
558
- *
559
- * @param result the service request result
560
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
561
- *
562
- * @private
563
- */
564
- export function _updateTemplateDictionaryForError(templateDictionary, itemId) {
565
- /* istanbul ignore else */
566
- if (itemId) {
567
- let removeKey = "";
568
- Object.keys(templateDictionary).some(k => {
569
- /* istanbul ignore else */
570
- if (templateDictionary[k].itemId === itemId) {
571
- removeKey = k;
572
- return true;
573
- }
574
- });
575
- /* istanbul ignore else */
576
- if (removeKey !== "") {
577
- delete templateDictionary[removeKey];
578
- }
579
- }
580
- return templateDictionary;
581
- }
582
- /**
583
- * Optionally search by tags and then update the templateDictionary based on the search results
584
- *
585
- * @param existingItemsResponse response object from search by typeKeyword and type
586
- * @param existingItemIds list of the template ids we have queried
587
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
588
- * @param authentication Credentials for the request
589
- * @param addTagQuery Boolean to indicate if a search by tag should happen
590
- * @returns IFindExistingItemsResponse object with promise that will resolve with an array of results
591
- * and an array of item ids
592
- * @private
593
- */
594
- export function _handleExistingItems(existingItemsResponse, existingItemInfos, templateDictionary, authentication, addTagQuery) {
595
- // if items are not found by type keyword search by tag
596
- const existingItemsByTag = [Promise.resolve(null)];
597
- const existingItemInfosByTag = [undefined];
598
- /* istanbul ignore else */
599
- if (existingItemsResponse && Array.isArray(existingItemsResponse)) {
600
- existingItemsResponse.forEach((existingItem, i) => {
601
- /* istanbul ignore else */
602
- if (Array.isArray(existingItem?.results)) {
603
- let result;
604
- const results = existingItem.results;
605
- if (results.length === 1) {
606
- result = results[0];
607
- }
608
- else if (results.length > 1) {
609
- result = results.reduce((a, b) => a.created > b.created ? a : b);
610
- }
611
- else {
612
- if (addTagQuery && existingItemInfos[i]) {
613
- const itemInfo = existingItemInfos[i];
614
- const tagQuery = `tags:source-${itemInfo.itemId} type:${itemInfo.type} owner:${templateDictionary.user.username}`;
615
- existingItemsByTag.push(_findExistingItem(tagQuery, authentication));
616
- existingItemInfosByTag.push(existingItemInfos[i]);
617
- }
618
- }
619
- if (result) {
620
- const sourceId = existingItemInfos[i].itemId;
621
- /* istanbul ignore else */
622
- if (sourceId) {
623
- _updateTemplateDictionaryById(templateDictionary, sourceId, result.id, result);
624
- }
625
- }
626
- }
627
- });
628
- }
629
- return {
630
- existingItemsDefs: existingItemsByTag,
631
- existingItemInfos: existingItemInfosByTag
632
- };
633
- }
634
- //???
635
- export function _updateTemplateDictionaryById(templateDictionary, sourceId, itemId, v) {
636
- templateDictionary[sourceId] = Object.assign(templateDictionary[sourceId] || {}, {
637
- def: Promise.resolve(common.generateEmptyCreationResponse(v.type, itemId)),
638
- itemId,
639
- name: v.name,
640
- title: v.title,
641
- url: v.url
642
- });
643
- }
644
- /**
645
- * Search items based on user query
646
- *
647
- * @param templates Templates to examine
648
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
649
- * @param authentication Credentials for the request
650
- * @returns IFindExistingItemsResponse object with promise that will resolve with an array of results
651
- * and an array of item ids
652
- * @private
653
- */
654
- export function _findExistingItemByKeyword(templates, templateDictionary, authentication) {
655
- const existingItemsDefs = [];
656
- const existingItemInfos = [];
657
- templates.forEach(template => {
658
- if (template.item.type === "Group") {
659
- const userGroups = templateDictionary.user?.groups;
660
- /* istanbul ignore else */
661
- if (Array.isArray(userGroups)) {
662
- existingItemsDefs.push(Promise.resolve({
663
- results: userGroups
664
- .filter(g => g.tags.indexOf(`source-${template.itemId}`) > -1 || g.typeKeywords.indexOf(`source-${template.itemId}`) > -1)
665
- .map(g => {
666
- g.type = "Group";
667
- return g;
668
- }),
669
- sourceId: template.itemId
670
- }));
671
- }
672
- }
673
- else {
674
- existingItemsDefs.push(_findExistingItem(`typekeywords:source-${template.itemId} type:${template.item.type} owner:${templateDictionary.user.username}`, authentication));
675
- }
676
- existingItemInfos.push({
677
- itemId: template.itemId,
678
- type: template.item.type
679
- });
680
- });
681
- return {
682
- existingItemsDefs,
683
- existingItemInfos
684
- };
685
- }
686
- /**
687
- * Search items based on user query
688
- *
689
- * @param query Query string to use
690
- * @param authentication Credentials for the request
691
- * @returns A promise that will resolve with an array of results
692
- * @private
693
- */
694
- export function _findExistingItem(query, authentication) {
695
- const searchOptions = {
696
- q: query,
697
- authentication: authentication,
698
- pagingParam: { start: 1, num: 100 }
699
- };
700
- return common.searchItems(searchOptions);
701
- }
702
- /**
703
- * Creates an item from a template once the item's dependencies have been created.
704
- *
705
- * @param template Template of item to deploy
706
- * @param resourceFilePaths URL, folder, and filename for each item resource/metadata/thumbnail
707
- * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
708
- * @param userSession Options for the request
709
- * @param itemProgressCallback Function for reporting progress updates from type-specific template handlers
710
- * @returns A promise that will resolve with the id of the deployed item (which is simply returned if it's
711
- * already in the templates list
712
- * @private
713
- */
714
- export function _createItemFromTemplateWhenReady(template, resourceFilePaths, storageAuthentication, templateDictionary, destinationAuthentication, itemProgressCallback) {
715
- // ensure this is present
716
- template.dependencies = template.dependencies || [];
717
- // if there is no entry in the templateDictionary
718
- // or if we have a basic entry without the deferred request for its creation, add it
719
- if (!templateDictionary.hasOwnProperty(template.itemId) ||
720
- !common.getProp(templateDictionary[template.itemId], "def")) {
721
- let createResponse;
722
- let statusCode = common.EItemProgressStatus.Unknown;
723
- let itemHandler;
724
- templateDictionary[template.itemId] =
725
- templateDictionary[template.itemId] || {};
726
- // Save the deferred for the use of items that depend on this item being created first
727
- templateDictionary[template.itemId].def = new Promise(resolve => {
728
- // Wait until all of the item's dependencies are deployed
729
- const _awaitDependencies = template.dependencies.reduce((acc, id) => {
730
- const def = common.getProp(templateDictionary, `${id}.def`);
731
- // can't use maybePush as that clones the object, which does not work for Promises
732
- /* istanbul ignore else */
733
- if (def) {
734
- acc.push(def);
735
- }
736
- return acc;
737
- }, []);
738
- const syncViews = common.getProp(template, "properties.syncViews");
739
- const awaitDependencies = syncViews && syncViews.length > 0
740
- ? syncViews.reduce((acc, v) => {
741
- const def = common.getProp(templateDictionary, `${v}.def`);
742
- /* istanbul ignore else */
743
- if (def) {
744
- acc.push(def);
745
- }
746
- return acc;
747
- }, _awaitDependencies)
748
- : _awaitDependencies;
749
- Promise.all(awaitDependencies)
750
- .then(() => {
751
- // Find the conversion handler for this item type
752
- const templateType = template.type;
753
- itemHandler = moduleMap[templateType];
754
- if (!itemHandler || itemHandler === UNSUPPORTED) {
755
- if (itemHandler === UNSUPPORTED) {
756
- statusCode = common.EItemProgressStatus.Ignored;
757
- throw new Error();
758
- }
759
- else {
760
- statusCode = common.EItemProgressStatus.Failed;
761
- throw new Error();
762
- }
763
- }
764
- // Get the item's thumbnail
765
- return common.getThumbnailFromStorageItem(storageAuthentication, resourceFilePaths);
766
- })
767
- .then(thumbnail => {
768
- template.item.thumbnail = thumbnail;
769
- // Delegate the creation of the item to the handler
770
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
771
- return itemHandler.createItemFromTemplate(template, templateDictionary, destinationAuthentication, itemProgressCallback);
772
- })
773
- .then((response) => {
774
- if (response.id === "") {
775
- statusCode = common.EItemProgressStatus.Failed;
776
- throw new Error("handled"); // fails to create item
777
- }
778
- /* istanbul ignore else */
779
- createResponse = response;
780
- if (createResponse.item.item.url) {
781
- common.setCreateProp(templateDictionary, template.itemId + ".url", createResponse.item.item.url);
782
- }
783
- if (resourceFilePaths.length > 0) {
784
- // Copy resources, metadata, form
785
- return common.copyFilesFromStorageItem(storageAuthentication, resourceFilePaths, template.itemId, templateDictionary.folderId, createResponse.id, destinationAuthentication, createResponse.item);
786
- }
787
- else {
788
- return Promise.resolve(null);
789
- }
790
- })
791
- .then(() => {
792
- resolve(createResponse);
793
- })
794
- .catch(error => {
795
- if (!error || error.message !== "handled") {
796
- itemProgressCallback(template.itemId, statusCode === common.EItemProgressStatus.Unknown
797
- ? common.EItemProgressStatus.Failed
798
- : statusCode, 0);
799
- }
800
- // Item type not supported or fails to get item dependencies
801
- resolve(common.generateEmptyCreationResponse(template.type));
802
- });
803
- });
804
- }
805
- return templateDictionary[template.itemId].def;
806
- }
807
- /**
808
- * Accumulates the estimated deployment cost of a set of templates.
809
- *
810
- * @param templates Templates to examine
811
- * @returns Sum of estimated deployment costs
812
- * @private
813
- */
814
- export function _estimateDeploymentCost(templates) {
815
- return templates.reduce((accumulatedEstimatedCost, template) => {
816
- return (accumulatedEstimatedCost + (template.estimatedDeploymentCostFactor || 1));
817
- }, 0);
818
- }
819
- //???
820
- // TODO: Return a Promise vs array of promises
821
- export function _getGroupUpdates(template, authentication, templateDictionary) {
822
- const groups = template.groups || [];
823
- return groups.map((sourceGroupId) => {
824
- return common.shareItem(templateDictionary[sourceGroupId].itemId, template.itemId, authentication, common.isTrackingViewTemplate(template) ? templateDictionary.locationTracking.owner : undefined);
825
- });
826
- }
1
+ /** @license
2
+ * Copyright 2018 Esri
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Manages deployment of items via the REST API.
18
+ *
19
+ * @module deployItems
20
+ */
21
+ import * as common from "@esri/solution-common";
22
+ import { moduleMap } from "./module-map";
23
+ const UNSUPPORTED = null;
24
+ // ------------------------------------------------------------------------------------------------------------------ //
25
+ /**
26
+ * Deploys a set of items defined by templates.
27
+ *
28
+ * @param portalSharingUrl Server/sharing
29
+ * @param storageItemId Id of storage item
30
+ * @param templates A collection of AGO item templates
31
+ * @param storageAuthentication Credentials for the organization with the source items
32
+ * @param templateDictionary Hash of facts: org URL, adlib replacements
33
+ * @param deployedSolutionId Id of deployed Solution item
34
+ * @param destinationAuthentication Credentials for the destination organization
35
+ * @param options Options to tune deployment
36
+ * @returns A promise that will resolve with the list of information about the created items
37
+ */
38
+ export function deploySolutionItems(portalSharingUrl, storageItemId, templates, storageAuthentication, templateDictionary, deployedSolutionId, destinationAuthentication, options) {
39
+ return new Promise((resolve, reject) => {
40
+ // Prepare feedback mechanism
41
+ const totalEstimatedCost = _estimateDeploymentCost(templates) + 1; // solution items, plus avoid divide by 0
42
+ let percentDone = 10; // allow for previous deployment work
43
+ const progressPercentStep = (99 - percentDone) / totalEstimatedCost; // leave some % for caller for wrapup
44
+ const failedTemplateItemIds = [];
45
+ const deployedItemIds = [];
46
+ let statusOK = true;
47
+ // TODO: move to separate fn
48
+ const itemProgressCallback = (itemId, status, costUsed, createdItemId // supplied when status is EItemProgressStatus.Created or .Finished
49
+ ) => {
50
+ percentDone += progressPercentStep * costUsed;
51
+ /* istanbul ignore else */
52
+ if (options.progressCallback) {
53
+ if (status === common.EItemProgressStatus.Finished) {
54
+ const event = {
55
+ event: common.SItemProgressStatus[status],
56
+ data: itemId
57
+ };
58
+ options.progressCallback(Math.round(percentDone), options.jobId, event);
59
+ }
60
+ else {
61
+ options.progressCallback(Math.round(percentDone), options.jobId);
62
+ }
63
+ }
64
+ /* istanbul ignore if */
65
+ if (options.consoleProgress) {
66
+ console.log(Date.now(), itemId, options.jobId ?? "", common.SItemProgressStatus[status], percentDone.toFixed(0) + "%", costUsed, createdItemId ? "==> " + createdItemId : "");
67
+ }
68
+ if (status === common.EItemProgressStatus.Created) {
69
+ deployedItemIds.push(createdItemId);
70
+ }
71
+ else if (status === common.EItemProgressStatus.Failed) {
72
+ failedTemplateItemIds.push(itemId);
73
+ console.error("Item " + itemId + " has failed");
74
+ statusOK = false;
75
+ }
76
+ return statusOK;
77
+ // ---------------------------------------------------------------------------------------------------------------
78
+ };
79
+ // portal does not allow views of a single source to be created at the same time
80
+ if (common.getProp(templateDictionary, "organization.isPortal")) {
81
+ templates = _evaluateSharedViewSources(templates);
82
+ }
83
+ // Create an ordered graph of the templates so that dependencies are created before the items that need them.
84
+ // Because cycles are permitted, we also keep track of items that need to be patched later because their
85
+ // dependencies are necessarily created after they are created.
86
+ const { buildOrder, itemsToBePatched } = common.topologicallySortItems(templates);
87
+ // For each item in order from no dependencies to dependent on other items,
88
+ // * replace template symbols using template dictionary
89
+ // * create item in destination group
90
+ // * add created item's id into the template dictionary
91
+ const awaitAllItems = [];
92
+ const reuseItemsDef = _reuseDeployedItems(templates, options.enableItemReuse ?? false, templateDictionary, destinationAuthentication);
93
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
94
+ reuseItemsDef.then(() => {
95
+ const useExistingItemsDef = _useExistingItems(templates, common.getProp(templateDictionary, "params.useExisting"), templateDictionary, destinationAuthentication);
96
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
97
+ useExistingItemsDef.then(() => {
98
+ templates = common.setNamesAndTitles(templates, templateDictionary.solutionItemId);
99
+ buildOrder.forEach((id) => {
100
+ // Get the item's template out of the list of templates
101
+ const template = common.findTemplateInList(templates, id);
102
+ if (template.type === "QuickCapture Project") {
103
+ // Remove qc.project.json files from the resources--we don't use them from solutions
104
+ template.resources = template.resources.filter((filename) => !filename.endsWith("qc.project.json"));
105
+ }
106
+ awaitAllItems.push(_createItemFromTemplateWhenReady(template, common.generateStorageFilePaths(portalSharingUrl, storageItemId, template.resources, options.storageVersion), storageAuthentication, templateDictionary, destinationAuthentication, itemProgressCallback));
107
+ });
108
+ // Wait until all items have been created
109
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
110
+ Promise.all(awaitAllItems).then((clonedSolutionItems) => {
111
+ if (failedTemplateItemIds.length === 0) {
112
+ // Do we have any items to be patched (i.e., they refer to dependencies using the template id rather
113
+ // than the cloned id because the item had to be created before the dependency)? Flag these items
114
+ // for post processing in the list of clones.
115
+ _flagPatchItemsForPostProcessing(itemsToBePatched, templateDictionary, clonedSolutionItems);
116
+ resolve(clonedSolutionItems);
117
+ }
118
+ else {
119
+ // Delete created items
120
+ const progressOptions = {
121
+ consoleProgress: true
122
+ };
123
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
124
+ common
125
+ .deleteSolutionByComponents(deployedSolutionId, deployedItemIds, templates, templateDictionary, destinationAuthentication, progressOptions)
126
+ .then(() => reject(common.failWithIds(failedTemplateItemIds)));
127
+ }
128
+ });
129
+ });
130
+ }, e => {
131
+ console.error(e);
132
+ reject(common.fail(e));
133
+ });
134
+ });
135
+ }
136
+ /**
137
+ * For each item to be patched, convert it to its cloned id and mark the item as needing post processing.
138
+ *
139
+ * @param itemsToBePatched List of items that need to have their dependencies patched
140
+ * @param templateDictionary Hash of facts: org URL, adlib replacements
141
+ * @param templates A collection of AGO item templates
142
+ * @private
143
+ */
144
+ export function _flagPatchItemsForPostProcessing(itemsToBePatched, templateDictionary, templates) {
145
+ let itemIdsToBePatched = Object.keys(itemsToBePatched);
146
+ /* istanbul ignore else */
147
+ if (itemIdsToBePatched.length > 0) {
148
+ // Replace the ids of the items to be patched (which are template ids) with their cloned versions
149
+ itemIdsToBePatched = itemIdsToBePatched.map(id => templateDictionary[id].itemId);
150
+ // Make sure that the items to be patched are flagged for post processing
151
+ templates.forEach(item => {
152
+ /* istanbul ignore else */
153
+ if (itemIdsToBePatched.includes(item.id)) {
154
+ item.postProcess = true;
155
+ }
156
+ });
157
+ }
158
+ }
159
+ /**
160
+ * Portal does not allow views of a single source to be created at the same time.
161
+ *
162
+ * Update view templates with an array of other view template ids that it should wait on.
163
+ *
164
+ * @param templates a collection of AGO item templates
165
+ *
166
+ * @returns An updated array of item templates
167
+ * @private
168
+ */
169
+ export function _evaluateSharedViewSources(templates) {
170
+ // update the templates so we can defer the deployment when more than one view shares the same source
171
+ // these are not classic dependencies but are in some ways similar
172
+ const views = _getViews(templates);
173
+ _updateViewTemplates(templates, views);
174
+ const viewHash = _getViewHash(views);
175
+ let processed = [];
176
+ const visited = [];
177
+ Object.keys(viewHash).forEach(k => {
178
+ const _views = viewHash[k];
179
+ _views.forEach(cv => {
180
+ const template = common.findTemplateInList(templates, cv);
181
+ const syncViews = common.getProp(template, "properties.syncViews");
182
+ /* istanbul ignore else */
183
+ if (visited.indexOf(template.itemId) > -1) {
184
+ processed = processed.concat(syncViews);
185
+ }
186
+ /* istanbul ignore else */
187
+ if (syncViews && syncViews.length > 0) {
188
+ // when a view has multiple dependencies we need to retain the syncViews if they have been set already...
189
+ common.setProp(template, "properties.syncViews", common.cloneObject(processed));
190
+ }
191
+ /* istanbul ignore else */
192
+ if (processed.indexOf(cv) < 0) {
193
+ processed.push(cv);
194
+ }
195
+ /* istanbul ignore else */
196
+ if (visited.indexOf(template.itemId) < 0) {
197
+ visited.push(template.itemId);
198
+ }
199
+ });
200
+ processed = [];
201
+ });
202
+ return templates;
203
+ }
204
+ /**
205
+ * Add a syncViews array to each template that will hold all other view ids that
206
+ * have the same FS dependency.
207
+ * These arrays will be processed later to only contain ids that each view will need to wait on.
208
+ *
209
+ * @param templates a collection of AGO item templates
210
+ * @param views an array of view template details
211
+ *
212
+ * @returns An updated array of item templates
213
+ * @private
214
+ */
215
+ export function _updateViewTemplates(templates, views) {
216
+ views.forEach(v => {
217
+ v.dependencies.forEach((id) => {
218
+ templates = templates.map(t => {
219
+ /* istanbul ignore else */
220
+ if (common.getProp(t, "properties.service.isView") &&
221
+ t.dependencies.indexOf(id) > -1 &&
222
+ t.itemId !== v.id) {
223
+ /* istanbul ignore else */
224
+ if (!Array.isArray(t.properties.syncViews)) {
225
+ t.properties.syncViews = [];
226
+ }
227
+ /* istanbul ignore else */
228
+ if (t.properties.syncViews.indexOf(v.id) < 0) {
229
+ t.properties.syncViews.push(v.id);
230
+ }
231
+ }
232
+ return t;
233
+ });
234
+ });
235
+ });
236
+ return templates;
237
+ }
238
+ /**
239
+ * Get all view templates from the source templates collection
240
+ *
241
+ * @param views A collection of view ID and dependencies
242
+ *
243
+ * @returns an array of objects with the source FS id as the key and a list of views that are
244
+ * dependant upon it
245
+ *
246
+ * @private
247
+ */
248
+ export function _getViewHash(views) {
249
+ const viewHash = {};
250
+ views.forEach(v => {
251
+ v.dependencies.forEach((d) => {
252
+ /* istanbul ignore else */
253
+ if (Object.keys(viewHash).indexOf(d) < 0) {
254
+ viewHash[d] = [v.id];
255
+ }
256
+ else if (viewHash[d].indexOf(v.id) < 0) {
257
+ viewHash[d].push(v.id);
258
+ }
259
+ });
260
+ });
261
+ return viewHash;
262
+ }
263
+ /**
264
+ * Get all view templates from the source templates collection
265
+ *
266
+ * @param templates A collection of AGO item templates
267
+ *
268
+ * @returns an array with the view id and its dependencies
269
+ *
270
+ * @private
271
+ */
272
+ export function _getViews(templates) {
273
+ return templates.reduce((acc, v) => {
274
+ /* istanbul ignore else */
275
+ if (common.getProp(v, "properties.service.isView")) {
276
+ acc.push({
277
+ id: v.itemId,
278
+ dependencies: v.dependencies
279
+ });
280
+ }
281
+ return acc;
282
+ }, []);
283
+ }
284
+ /**
285
+ * Search for existing items and update the templateDictionary with key details
286
+ *
287
+ * @param templates A collection of AGO item templates
288
+ * @param reuseItems Option to search for existing items
289
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
290
+ * @param authentication Credentials for the requests
291
+ *
292
+ * @returns A Promise that will resolve once existing items have been evaluated
293
+ *
294
+ * @private
295
+ */
296
+ export function _reuseDeployedItems(templates, reuseItems, templateDictionary, authentication) {
297
+ return new Promise((resolve, reject) => {
298
+ if (reuseItems) {
299
+ const findItemsByKeywordResponse = _findExistingItemByKeyword(templates, templateDictionary, authentication);
300
+ const existingItemsByKeyword = findItemsByKeywordResponse.existingItemsDefs;
301
+ const existingItemInfos = findItemsByKeywordResponse.existingItemInfos;
302
+ Promise.all(existingItemsByKeyword).then((existingItemsByKeywordResponse) => {
303
+ const findExistingItemsByTag = _handleExistingItems(existingItemsByKeywordResponse, existingItemInfos, templateDictionary, authentication, true);
304
+ const existingItemsByTag = findExistingItemsByTag.existingItemsDefs;
305
+ const existingItemInfosByTag = findExistingItemsByTag.existingItemInfos;
306
+ Promise.all(existingItemsByTag).then(existingItemsByTagResponse => {
307
+ _handleExistingItems(existingItemsByTagResponse, existingItemInfosByTag, templateDictionary, authentication, false);
308
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
309
+ _updateTemplateDictionary(templates, templateDictionary, authentication).then(resolve);
310
+ }, e => reject(common.fail(e)));
311
+ }, e => reject(common.fail(e)));
312
+ }
313
+ else {
314
+ resolve(null);
315
+ }
316
+ });
317
+ }
318
+ /**
319
+ * Search for existing items and update the templateDictionary with key details
320
+ *
321
+ * Subtle difference between _reuseDeployedItems and _useExistingItems
322
+ * _reuseDeployedItems: will search all existing items based on specific type keywords
323
+ * that would have been added by a previous deployment
324
+ * _useExistingItems: will search for an existing item that the user provided
325
+ * the item id for while configuring in the deployment app.
326
+ * This type of item would not necessarily have been laid down by a previous deployment and
327
+ * can thus not expect that it will have the type keywords
328
+ *
329
+ * @param templates A collection of AGO item templates
330
+ * @param useExisting Option to search for existing items
331
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
332
+ * @param authentication Credentials for the requests
333
+ *
334
+ * @returns A Promise that will resolve once existing items have been evaluated
335
+ *
336
+ * @private
337
+ */
338
+ export function _useExistingItems(templates, useExisting, templateDictionary, authentication) {
339
+ return new Promise(resolve => {
340
+ if (useExisting) {
341
+ const itemDefs = [];
342
+ const sourceIdHash = {};
343
+ const itemIds = [];
344
+ Object.keys(templateDictionary.params).forEach(k => {
345
+ const v = templateDictionary.params[k];
346
+ /* istanbul ignore else */
347
+ if (v.itemId && /[0-9A-F]{32}/i.test(k)) {
348
+ _updateTemplateDictionaryById(templateDictionary, k, v.itemId, v);
349
+ // need to check and set the typeKeyword if it doesn't exist on this service yet
350
+ // when the user has passed in an itemId that does not come from a previous deployment
351
+ itemDefs.push(common.getItemBase(v.itemId, authentication));
352
+ sourceIdHash[v.itemId] = k;
353
+ /* istanbul ignore else */
354
+ if (itemIds.indexOf(k) < 0) {
355
+ itemIds.push(k);
356
+ }
357
+ }
358
+ });
359
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
360
+ _setTypekeywordForExisting(itemDefs, sourceIdHash, authentication).then(() => {
361
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
362
+ _updateTemplateDictionary(itemIds.map(id => common.getTemplateById(templates, id)), templateDictionary, authentication).then(resolve);
363
+ });
364
+ }
365
+ else {
366
+ resolve(null);
367
+ }
368
+ });
369
+ }
370
+ /**
371
+ * Verify if the existing item has the source-<itemId> typeKeyword and set it if not
372
+ * This allows items that did not come from deployment to be found for reuse after they
373
+ * have been used once via a custom itemId param
374
+ *
375
+ * @param itemDefs
376
+ * @param sourceIdHash key value pairs..actual itemId is the key and the source itemId is the value
377
+ * @param authentication credentials for the requests
378
+ *
379
+ * @returns a promise to indicate when the requests are complete
380
+ * @private
381
+ */
382
+ export function _setTypekeywordForExisting(itemDefs, sourceIdHash, authentication) {
383
+ return new Promise(resolve => {
384
+ if (itemDefs.length > 0) {
385
+ Promise.all(itemDefs).then(results => {
386
+ const itemUpdateDefs = [];
387
+ results.forEach(result => {
388
+ const sourceId = sourceIdHash[result.id];
389
+ /* istanbul ignore else */
390
+ if (result && sourceId && result.typeKeywords) {
391
+ const sourceKeyword = `source-${sourceId}`;
392
+ const typeKeywords = result.typeKeywords;
393
+ /* istanbul ignore else */
394
+ if (typeKeywords.indexOf(sourceKeyword) < 0) {
395
+ typeKeywords.push(sourceKeyword);
396
+ const itemUpdate = { id: result.id, typeKeywords };
397
+ itemUpdateDefs.push(common.updateItem(itemUpdate, authentication));
398
+ }
399
+ }
400
+ });
401
+ // wait for updates to finish before we resolve
402
+ if (itemUpdateDefs.length > 0) {
403
+ Promise.all(itemUpdateDefs).then(resolve, () => resolve(undefined));
404
+ }
405
+ else {
406
+ resolve(undefined);
407
+ }
408
+ }, () => resolve(undefined));
409
+ }
410
+ else {
411
+ resolve(undefined);
412
+ }
413
+ });
414
+ }
415
+ /**
416
+ * Update the templateDictionary with key details by item type
417
+ *
418
+ * @param templates A collection of AGO item templates
419
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
420
+ *
421
+ * @private
422
+ */
423
+ export function _updateTemplateDictionary(templates, templateDictionary, authentication) {
424
+ return new Promise(resolve => {
425
+ const defs = [];
426
+ const urls = [];
427
+ const types = [];
428
+ const ids = [];
429
+ templates.forEach(t => {
430
+ const templateInfo = templateDictionary[t.itemId];
431
+ /* istanbul ignore else */
432
+ if (templateInfo && templateInfo.url && templateInfo.itemId) {
433
+ /* istanbul ignore else */
434
+ if (t.item.type === "Feature Service") {
435
+ const enterpriseIDMapping = common.getProp(templateDictionary, `params.${t.itemId}.enterpriseIDMapping`);
436
+ Object.assign(templateDictionary[t.itemId], common.getLayerSettings(common.getLayersAndTables(t), templateInfo.url, templateInfo.itemId, enterpriseIDMapping));
437
+ // if the service has veiws keep track of the fields so we can use them to
438
+ // compare with the view fields
439
+ /* istanbul ignore else */
440
+ if (common.getProp(t, "properties.service.hasViews")) {
441
+ common._updateTemplateDictionaryFields(t, templateDictionary, false);
442
+ }
443
+ }
444
+ // for fs query with its url...for non fs query the item
445
+ // this is to verify situations where we have a stale search index that will
446
+ // say some items exist when they don't really exist
447
+ // searching the services url or with the item id will return an error when this condition occurs
448
+ /* istanbul ignore else */
449
+ if (urls.indexOf(templateInfo.url) < 0) {
450
+ defs.push(t.item.type === "Feature Service"
451
+ ? common.rest_request(templateInfo.url, { authentication })
452
+ : common.getItemBase(templateInfo.itemId, authentication));
453
+ urls.push(templateInfo.url);
454
+ types.push(t.item.type);
455
+ ids.push(templateInfo.itemId);
456
+ }
457
+ }
458
+ });
459
+ if (defs.length > 0) {
460
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
461
+ Promise.all(defs.map(p => p.catch(e => e))).then(results => {
462
+ /* istanbul ignore else */
463
+ if (Array.isArray(results) && results.length > 0) {
464
+ const fieldDefs = [];
465
+ results.forEach((r, i) => {
466
+ // a feature service result will contain a serviceItemId if it was successfully fetched
467
+ if (r.serviceItemId && types[i] === "Feature Service") {
468
+ Object.keys(templateDictionary).forEach(k => {
469
+ const v = templateDictionary[k];
470
+ /* istanbul ignore else */
471
+ if (v.itemId && v.itemId === r.serviceItemId) {
472
+ common.setDefaultSpatialReference(templateDictionary, k, r.spatialReference);
473
+ // keep the extent values from these responses as well
474
+ common.setCreateProp(templateDictionary, `${k}.defaultExtent`, r.fullExtent || r.initialExtent);
475
+ const layerIds = (r.layers || []).map((l) => l.id);
476
+ const tablesIds = (r.tables || []).map((t) => t.id);
477
+ fieldDefs.push(common.getExistingLayersAndTables(urls[i], layerIds.concat(tablesIds), authentication));
478
+ }
479
+ });
480
+ }
481
+ else {
482
+ /* istanbul ignore else */
483
+ if (types[i] === "Feature Service" ||
484
+ common.getProp(r, "response.error")) {
485
+ // if an error is returned we need to clean up the templateDictionary
486
+ templateDictionary = _updateTemplateDictionaryForError(templateDictionary, ids[i]);
487
+ }
488
+ }
489
+ });
490
+ if (fieldDefs.length > 0) {
491
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
492
+ Promise.all(fieldDefs).then(layerTableResult => {
493
+ layerTableResult.forEach(l => {
494
+ l.forEach((ll) => {
495
+ Object.keys(templateDictionary).forEach(k => {
496
+ /* istanbul ignore else */
497
+ if (templateDictionary[k].itemId === ll.serviceItemId) {
498
+ let sourceId = "";
499
+ Object.keys(templateDictionary).some(_k => {
500
+ /* istanbul ignore else */
501
+ if (templateDictionary[_k].itemId === ll.serviceItemId) {
502
+ sourceId = _k;
503
+ return true;
504
+ }
505
+ });
506
+ const enterpriseIDMapping = common.getProp(templateDictionary, `params.${sourceId}.enterpriseIDMapping`);
507
+ if (enterpriseIDMapping) {
508
+ Object.keys(enterpriseIDMapping).forEach(id => {
509
+ if (enterpriseIDMapping[id].toString() ===
510
+ ll.id.toString()) {
511
+ _setFields(templateDictionary, k, id, ll.fields);
512
+ }
513
+ });
514
+ }
515
+ else {
516
+ _setFields(templateDictionary, k, ll.id, ll.fields);
517
+ }
518
+ }
519
+ });
520
+ });
521
+ });
522
+ resolve(null);
523
+ });
524
+ }
525
+ else {
526
+ resolve(null);
527
+ }
528
+ }
529
+ else {
530
+ resolve(null);
531
+ }
532
+ });
533
+ }
534
+ else {
535
+ resolve(null);
536
+ }
537
+ });
538
+ }
539
+ /**
540
+ * Add the fields from the source layer to the template dictionary for any required replacements
541
+ *
542
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
543
+ * @param itemId the id for the item
544
+ * @param layerId the id for the layer
545
+ * @param fields the fields to transfer
546
+ *
547
+ * @private
548
+ */
549
+ export function _setFields(templateDictionary, itemId, layerId, fields) {
550
+ const layerInfo = common.getProp(templateDictionary, `${itemId}.layer${layerId}`);
551
+ /* istanbul ignore else */
552
+ if (layerInfo && fields) {
553
+ layerInfo.fields = fields;
554
+ }
555
+ }
556
+ /**
557
+ * In some cases an item id search will return a stale item reference
558
+ * it will subsequently fail when we try to fetch the underlying service.
559
+ *
560
+ * We need to remove the item info that has been added to the template dictionary
561
+ * and treat the item as we do other items that don't already exist on deployment.
562
+ *
563
+ * @param result the service request result
564
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
565
+ *
566
+ * @private
567
+ */
568
+ export function _updateTemplateDictionaryForError(templateDictionary, itemId) {
569
+ /* istanbul ignore else */
570
+ if (itemId) {
571
+ let removeKey = "";
572
+ Object.keys(templateDictionary).some(k => {
573
+ /* istanbul ignore else */
574
+ if (templateDictionary[k].itemId === itemId) {
575
+ removeKey = k;
576
+ return true;
577
+ }
578
+ });
579
+ /* istanbul ignore else */
580
+ if (removeKey !== "") {
581
+ delete templateDictionary[removeKey];
582
+ }
583
+ }
584
+ return templateDictionary;
585
+ }
586
+ /**
587
+ * Optionally search by tags and then update the templateDictionary based on the search results
588
+ *
589
+ * @param existingItemsResponse response object from search by typeKeyword and type
590
+ * @param existingItemIds list of the template ids we have queried
591
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
592
+ * @param authentication Credentials for the request
593
+ * @param addTagQuery Boolean to indicate if a search by tag should happen
594
+ * @returns IFindExistingItemsResponse object with promise that will resolve with an array of results
595
+ * and an array of item ids
596
+ * @private
597
+ */
598
+ export function _handleExistingItems(existingItemsResponse, existingItemInfos, templateDictionary, authentication, addTagQuery) {
599
+ // if items are not found by type keyword search by tag
600
+ const existingItemsByTag = [Promise.resolve(null)];
601
+ const existingItemInfosByTag = [undefined];
602
+ /* istanbul ignore else */
603
+ if (existingItemsResponse && Array.isArray(existingItemsResponse)) {
604
+ existingItemsResponse.forEach((existingItem, i) => {
605
+ /* istanbul ignore else */
606
+ if (Array.isArray(existingItem?.results)) {
607
+ let result;
608
+ const results = existingItem.results;
609
+ if (results.length === 1) {
610
+ result = results[0];
611
+ }
612
+ else if (results.length > 1) {
613
+ result = results.reduce((a, b) => a.created > b.created ? a : b);
614
+ }
615
+ else {
616
+ if (addTagQuery && existingItemInfos[i]) {
617
+ const itemInfo = existingItemInfos[i];
618
+ const tagQuery = `tags:source-${itemInfo.itemId} type:${itemInfo.type} owner:${templateDictionary.user.username}`;
619
+ existingItemsByTag.push(_findExistingItem(tagQuery, authentication));
620
+ existingItemInfosByTag.push(existingItemInfos[i]);
621
+ }
622
+ }
623
+ if (result) {
624
+ const sourceId = existingItemInfos[i].itemId;
625
+ /* istanbul ignore else */
626
+ if (sourceId) {
627
+ _updateTemplateDictionaryById(templateDictionary, sourceId, result.id, result);
628
+ }
629
+ }
630
+ }
631
+ });
632
+ }
633
+ return {
634
+ existingItemsDefs: existingItemsByTag,
635
+ existingItemInfos: existingItemInfosByTag
636
+ };
637
+ }
638
+ //TODO: function doc
639
+ export function _updateTemplateDictionaryById(templateDictionary, sourceId, itemId, v) {
640
+ templateDictionary[sourceId] = Object.assign(templateDictionary[sourceId] || {}, {
641
+ def: Promise.resolve(common.generateEmptyCreationResponse(v.type, itemId)),
642
+ itemId,
643
+ name: v.name,
644
+ title: v.title,
645
+ url: v.url
646
+ });
647
+ }
648
+ /**
649
+ * Search items based on user query
650
+ *
651
+ * @param templates Templates to examine
652
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
653
+ * @param authentication Credentials for the request
654
+ * @returns IFindExistingItemsResponse object with promise that will resolve with an array of results
655
+ * and an array of item ids
656
+ * @private
657
+ */
658
+ export function _findExistingItemByKeyword(templates, templateDictionary, authentication) {
659
+ const existingItemsDefs = [];
660
+ const existingItemInfos = [];
661
+ templates.forEach(template => {
662
+ if (template.item.type === "Group") {
663
+ const userGroups = templateDictionary.user?.groups;
664
+ /* istanbul ignore else */
665
+ if (Array.isArray(userGroups)) {
666
+ existingItemsDefs.push(Promise.resolve({
667
+ results: userGroups
668
+ .filter(g => g.tags.indexOf(`source-${template.itemId}`) > -1 || g.typeKeywords.indexOf(`source-${template.itemId}`) > -1)
669
+ .map(g => {
670
+ g.type = "Group";
671
+ return g;
672
+ }),
673
+ sourceId: template.itemId
674
+ }));
675
+ }
676
+ }
677
+ else {
678
+ existingItemsDefs.push(_findExistingItem(`typekeywords:source-${template.itemId} type:${template.item.type} owner:${templateDictionary.user.username}`, authentication));
679
+ }
680
+ existingItemInfos.push({
681
+ itemId: template.itemId,
682
+ type: template.item.type
683
+ });
684
+ });
685
+ return {
686
+ existingItemsDefs,
687
+ existingItemInfos
688
+ };
689
+ }
690
+ /**
691
+ * Search items based on user query
692
+ *
693
+ * @param query Query string to use
694
+ * @param authentication Credentials for the request
695
+ * @returns A promise that will resolve with an array of results
696
+ * @private
697
+ */
698
+ export function _findExistingItem(query, authentication) {
699
+ const searchOptions = {
700
+ q: query,
701
+ authentication: authentication,
702
+ pagingParam: { start: 1, num: 100 }
703
+ };
704
+ return common.searchItems(searchOptions);
705
+ }
706
+ /**
707
+ * Creates an item from a template once the item's dependencies have been created.
708
+ *
709
+ * @param template Template of item to deploy
710
+ * @param resourceFilePaths URL, folder, and filename for each item resource/metadata/thumbnail
711
+ * @param templateDictionary Hash of facts: org URL, adlib replacements, deferreds for dependencies
712
+ * @param userSession Options for the request
713
+ * @param itemProgressCallback Function for reporting progress updates from type-specific template handlers
714
+ * @returns A promise that will resolve with the id of the deployed item (which is simply returned if it's
715
+ * already in the templates list
716
+ * @private
717
+ */
718
+ export function _createItemFromTemplateWhenReady(template, resourceFilePaths, storageAuthentication, templateDictionary, destinationAuthentication, itemProgressCallback) {
719
+ // ensure this is present
720
+ template.dependencies = template.dependencies || [];
721
+ // if there is no entry in the templateDictionary
722
+ // or if we have a basic entry without the deferred request for its creation, add it
723
+ if (!templateDictionary.hasOwnProperty(template.itemId) ||
724
+ !common.getProp(templateDictionary[template.itemId], "def")) {
725
+ let createResponse;
726
+ let statusCode = common.EItemProgressStatus.Unknown;
727
+ let itemHandler;
728
+ templateDictionary[template.itemId] =
729
+ templateDictionary[template.itemId] || {};
730
+ // Save the deferred for the use of items that depend on this item being created first
731
+ templateDictionary[template.itemId].def = new Promise(resolve => {
732
+ // Wait until all of the item's dependencies are deployed
733
+ const _awaitDependencies = template.dependencies.reduce((acc, id) => {
734
+ const def = common.getProp(templateDictionary, `${id}.def`);
735
+ // can't use maybePush as that clones the object, which does not work for Promises
736
+ /* istanbul ignore else */
737
+ if (def) {
738
+ acc.push(def);
739
+ }
740
+ return acc;
741
+ }, []);
742
+ const syncViews = common.getProp(template, "properties.syncViews");
743
+ const awaitDependencies = syncViews && syncViews.length > 0
744
+ ? syncViews.reduce((acc, v) => {
745
+ const def = common.getProp(templateDictionary, `${v}.def`);
746
+ /* istanbul ignore else */
747
+ if (def) {
748
+ acc.push(def);
749
+ }
750
+ return acc;
751
+ }, _awaitDependencies)
752
+ : _awaitDependencies;
753
+ Promise.all(awaitDependencies)
754
+ .then(() => {
755
+ // Find the conversion handler for this item type
756
+ const templateType = template.type;
757
+ itemHandler = moduleMap[templateType];
758
+ if (!itemHandler || itemHandler === UNSUPPORTED) {
759
+ if (itemHandler === UNSUPPORTED) {
760
+ statusCode = common.EItemProgressStatus.Ignored;
761
+ throw new Error();
762
+ }
763
+ else {
764
+ statusCode = common.EItemProgressStatus.Failed;
765
+ throw new Error();
766
+ }
767
+ }
768
+ // Get the item's thumbnail
769
+ return common.getThumbnailFromStorageItem(storageAuthentication, resourceFilePaths);
770
+ })
771
+ .then(thumbnail => {
772
+ template.item.thumbnail = thumbnail;
773
+ // Delegate the creation of the item to the handler
774
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
775
+ return itemHandler.createItemFromTemplate(template, templateDictionary, destinationAuthentication, itemProgressCallback);
776
+ })
777
+ .then((response) => {
778
+ if (response.id === "") {
779
+ statusCode = common.EItemProgressStatus.Failed;
780
+ throw new Error("handled"); // fails to create item
781
+ }
782
+ /* istanbul ignore else */
783
+ createResponse = response;
784
+ if (createResponse.item.item.url) {
785
+ common.setCreateProp(templateDictionary, template.itemId + ".url", createResponse.item.item.url);
786
+ }
787
+ if (resourceFilePaths.length > 0) {
788
+ // Copy resources, metadata, form
789
+ return common.copyFilesFromStorageItem(storageAuthentication, resourceFilePaths, template.itemId, templateDictionary.folderId, createResponse.id, destinationAuthentication, createResponse.item);
790
+ }
791
+ else {
792
+ return Promise.resolve(null);
793
+ }
794
+ })
795
+ .then(() => {
796
+ resolve(createResponse);
797
+ })
798
+ .catch(error => {
799
+ if (!error || error.message !== "handled") {
800
+ itemProgressCallback(template.itemId, statusCode === common.EItemProgressStatus.Unknown
801
+ ? common.EItemProgressStatus.Failed
802
+ : statusCode, 0);
803
+ }
804
+ // Item type not supported or fails to get item dependencies
805
+ resolve(common.generateEmptyCreationResponse(template.type));
806
+ });
807
+ });
808
+ }
809
+ return templateDictionary[template.itemId].def;
810
+ }
811
+ /**
812
+ * Accumulates the estimated deployment cost of a set of templates.
813
+ *
814
+ * @param templates Templates to examine
815
+ * @returns Sum of estimated deployment costs
816
+ * @private
817
+ */
818
+ export function _estimateDeploymentCost(templates) {
819
+ return templates.reduce((accumulatedEstimatedCost, template) => {
820
+ return (accumulatedEstimatedCost + (template.estimatedDeploymentCostFactor || 1));
821
+ }, 0);
822
+ }
823
+ //TODO: function doc
824
+ // TODO: Return a Promise vs array of promises
825
+ export function _getGroupUpdates(template, authentication, templateDictionary) {
826
+ const groups = template.groups || [];
827
+ return groups.map((sourceGroupId) => {
828
+ return common.shareItem(templateDictionary[sourceGroupId].itemId, template.itemId, authentication, common.isTrackingViewTemplate(template) ? templateDictionary.locationTracking.owner : undefined);
829
+ });
830
+ }
827
831
  //# sourceMappingURL=deploySolutionItems.js.map