UNPKG

@esri/solution-common

Version:

Provides general helper functions for @esri/solution.js.

1,087 lines 89.8 kB
"use strict"; /** @license * Copyright 2018 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports._countRelationships = exports._addItemMetadataFile = exports._addItemDataFile = exports.updateItemURL = exports.updateItemTemplateFromDictionary = exports.updateItemExtended = exports.updateGroup = exports.updateItem = exports.shareItem = exports.removeUsers = exports.reassignGroup = exports.searchGroupContents = exports.searchGroupAllContents = exports.searchAllGroups = exports.searchGroups = exports.searchAllItems = exports.searchItems = exports.removeItemOrGroup = exports.removeItem = exports.removeGroup = exports.removeFolder = exports.hasInvalidGroupDesignations = exports._parseAdminServiceData = exports.setWorkflowConfigurationZip = exports.getWorkflowConfigurationZip = exports.getFeatureServiceProperties = exports.getServiceLayersAndTables = exports.getRequest = exports._sortRelationships = exports.moveItemsToFolder = exports.moveItemToFolder = exports.getLayerUpdates = exports.getLayers = exports.extractDependencies = exports.createUniqueGroup = exports.createUniqueFolder = exports.createItemWithData = exports.createFullItem = exports.createFeatureService = exports.convertExtent = exports.convertExtentWithFallback = exports._validateExtent = exports.convertToISearchOptions = exports.checkRequestStatus = exports.addToServiceDefinition = exports.addTokenToUrl = exports.addForwardItemRelationships = exports.addForwardItemRelationship = exports.getUserSession = exports.addItemData = void 0; exports._updateItemURL = exports._updateIndexesForRelationshipKeyFields = exports._setItemProperties = exports._reportVariablesInItem = exports._lowercaseDomain = exports._getUpdate = exports._getSubtypeUpdates = exports._getContingentValuesUpdates = exports._getRelationshipUpdates = exports._getFallbackExtent = exports._getCreateServiceOptions = void 0; /** * Provides common functions involving the arcgis-rest-js library. * * @module restHelpers */ const featureServiceHelpers_1 = require("./featureServiceHelpers"); const generalHelpers_1 = require("./generalHelpers"); const arcgisRestJS_1 = require("./arcgisRestJS"); const libConnectors_1 = require("./libConnectors"); const restHelpersGet_1 = require("./restHelpersGet"); const workforceHelpers_1 = require("./workforceHelpers"); const templatization_1 = require("./templatization"); const trackingHelpers_1 = require("./trackingHelpers"); // ------------------------------------------------------------------------------------------------------------------ // function addItemData(id, data, authentication) { const addDataOptions = { id, authentication, }; if (data instanceof File) { addDataOptions["file"] = data; } else { addDataOptions["text"] = data; } return (0, arcgisRestJS_1.restAddItemData)(addDataOptions); } exports.addItemData = addItemData; /** * Creates a ArcGISIdentityManager via a function so that the global arcgisSolution variable can access authentication. * * @param options See https://developers.arcgis.com/arcgis-rest-js/api-reference/arcgis-rest-request/IArcGISIdentityManagerOptions/ * @returns UserSession (ArcGISIdentityManager) */ function getUserSession(options = {}) { return new arcgisRestJS_1.UserSession(options); } exports.getUserSession = getUserSession; /** * Adds a forward relationship between two items. * * @param originItemId Origin of relationship * @param destinationItemId Destination of relationship * @param relationshipType Type of relationship * @param authentication Credentials for the request * @returns A Promise to add item resources. */ function addForwardItemRelationship(originItemId, destinationItemId, relationshipType, authentication) { return new Promise((resolve) => { const requestOptions = { originItemId, destinationItemId, relationshipType, authentication, }; (0, arcgisRestJS_1.addItemRelationship)(requestOptions).then((response) => { resolve({ success: response.success, itemId: originItemId, }); }, () => { resolve({ success: false, itemId: originItemId, }); }); }); } exports.addForwardItemRelationship = addForwardItemRelationship; /** * Adds forward relationships for an item. * * @param originItemId Origin of relationship * @param destinationRelationships Destinations * @param authentication Credentials for the request * @returns A Promise to add item resources. */ function addForwardItemRelationships(originItemId, destinationRelationships, authentication) { return new Promise((resolve) => { // Set up relationships using updated relationship information const relationshipPromises = new Array(); destinationRelationships.forEach((relationship) => { relationship.relatedItemIds.forEach((relatedItemId) => { relationshipPromises.push(addForwardItemRelationship(originItemId, relatedItemId, relationship.relationshipType, authentication)); }); }); // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all(relationshipPromises).then((responses) => resolve(responses)); }); } exports.addForwardItemRelationships = addForwardItemRelationships; /** * Adds a token to the query parameters of a URL. * * @param url URL to use as base * @param authentication Credentials to be used to generate token for URL * @returns A promise that will resolve with the supplied URL with `token=<token>` added to its query params * unless either the URL doesn't exist or the token can't be generated */ function addTokenToUrl(url, authentication) { return new Promise((resolve) => { if (!url || !authentication) { resolve(url); } else { authentication.getToken(url).then((token) => { /* istanbul ignore else */ if (token) { url = (0, generalHelpers_1.appendQueryParam)(url, "token=" + token); } resolve(url); }, () => resolve(url)); } }); } exports.addTokenToUrl = addTokenToUrl; /** * Calls addToDefinition for the service. * * Added retry due to some solutions failing to deploy in specific orgs/hives due to timeouts. * On the first pass we will use the quicker sync request to add. * If it fails we will use an async request that will avoid the timeout errors. * * @param url URL to use as base * @param options the info to add to the services definition * @param skipRetry a boolean to control if retry logic will be used. Defaults to false. * @param useAsync a boolean to control if we will use an async request * @returns A promise that will resolve when the request has completed */ function addToServiceDefinition(url, options, skipRetry = false, useAsync = false) { /* istanbul ignore else */ if (useAsync) { options.params = { ...options.params, async: true }; } return new Promise((resolve, reject) => { (0, arcgisRestJS_1.svcAdminAddToServiceDefinition)(url, options).then((result) => { checkRequestStatus(result, options.authentication).then(() => resolve(null), (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => { if (!skipRetry) { addToServiceDefinition(url, options, true, true).then(() => resolve(null), (e) => reject(e)); } else { reject((0, generalHelpers_1.fail)(e)); } }); }); } exports.addToServiceDefinition = addToServiceDefinition; /** * When using an async request we need to poll the status url to know when the request has completed or failed. * * @param result the result returned from the addToDefinition request. * This will contain a status url or the standard sync result. * @param authentication Credentials to be used to generate token for URL * @returns A promise that will resolve when the request has completed */ function checkRequestStatus(result, authentication) { return new Promise((resolve, reject) => { const url = result.statusURL || result.statusUrl; if (url) { const checkStatus = setInterval(() => { (0, arcgisRestJS_1.request)(url, { authentication }).then((r) => { /* istanbul ignore else */ if (["completed", "success"].indexOf(r.status.toLowerCase()) > -1) { clearInterval(checkStatus); resolve(); } else if (r.status.toLowerCase() === "failed") { clearInterval(checkStatus); reject(r); } }, (e) => { clearInterval(checkStatus); reject(e); }); }, 2000); } else { resolve(); } }); } exports.checkRequestStatus = checkRequestStatus; /** * Converts a general search into an ISearchOptions structure. * * @param search Search specified in one of three ways * @returns Recast search */ function convertToISearchOptions(search) { // Convert the search into an ISearchOptions let searchOptions = { q: "", start: 1, num: 100, }; if (typeof search === "string") { // Insert query into defaults searchOptions.q = search; } else if (search instanceof arcgisRestJS_1.SearchQueryBuilder) { // Insert query into defaults searchOptions.q = search.toParam(); } else { // search is ISearchOptions searchOptions = { ...searchOptions, ...search, // request }; } // Remove the sortField if it's "relevance"; that's the default option and is not meant to be specified if (searchOptions.sortField === "relevance") { delete searchOptions.sortField; } return searchOptions; } exports.convertToISearchOptions = convertToISearchOptions; /** * Simple validate function to ensure all coordinates are numbers * In some cases orgs can have null or undefined coordinate values associated with the org extent * * @param extent the extent to validate * @returns the provided extent or a default global extent if some coordinates are not numbers * @private */ function _validateExtent(extent) { // in some cases orgs can have invalid extents defined // this is a simple validate function that will ensure coordiantes are numbers // using -179,-89,179,89 because the project call is returning "NaN" when using -180,-90,180,90 const hasInvalid = typeof extent.xmin !== "number" || typeof extent.xmax !== "number" || typeof extent.ymax !== "number" || typeof extent.ymin !== "number"; if (hasInvalid) { extent.xmin = -179; extent.xmax = 179; extent.ymax = 89; extent.ymin = -89; extent.spatialReference = { wkid: 4326 }; } return extent; } exports._validateExtent = _validateExtent; /** * If the request to convert the extent fails it has commonly been due to an invalid extent. * This function will first attempt to use the provided extent. If it fails it will default to * the source items extent and if that fails it will then use a default global extent. * * @param extent the extent to convert * @param fallbackExtent the extent to convert if the main extent does not project to the outSR * @param outSR the spatial reference to project to * @param geometryServiceUrl the service url for the geometry service to use * @param authentication the credentials for the requests * @returns the extent projected to the provided spatial reference * or the world extent projected to the provided spatial reference * @private */ function convertExtentWithFallback(extent, fallbackExtent, outSR, geometryServiceUrl, authentication) { return new Promise((resolve, reject) => { const defaultExtent = { xmin: -179, xmax: 179, ymin: -89, ymax: 89, spatialReference: { wkid: 4326 }, }; convertExtent(_validateExtent(extent), outSR, geometryServiceUrl, authentication).then((extentResponse) => { // in some cases project will complete successfully but return "NaN" values // check for this and call convert again if it does const extentResponseString = JSON.stringify(extentResponse); const validatedExtent = JSON.stringify(_validateExtent(extentResponse)); if (extentResponseString === validatedExtent) { resolve(extentResponse); } else { convertExtent(fallbackExtent || defaultExtent, outSR, geometryServiceUrl, authentication).then(resolve, (e) => reject((0, generalHelpers_1.fail)(e))); } }, // if convert fails try again with default global extent () => { convertExtent(defaultExtent, outSR, geometryServiceUrl, authentication).then(resolve, (e) => reject((0, generalHelpers_1.fail)(e))); }); }); } exports.convertExtentWithFallback = convertExtentWithFallback; /** * Converts an extent to a specified spatial reference. * * @param extent Extent object to check and (possibly) to project * @param outSR Desired spatial reference * @param geometryServiceUrl Path to geometry service providing `findTransformations` and `project` services * @param authentication Credentials for the request * @returns Original extent if it's already using outSR or the extents projected into the outSR */ function convertExtent(extent, outSR, geometryServiceUrl, authentication) { const _requestOptions = { authentication, httpMethod: "GET", }; return new Promise((resolve, reject) => { if (extent.spatialReference.wkid === outSR?.wkid || !outSR) { resolve(extent); } else { _requestOptions.params = { f: "json", inSR: extent.spatialReference.wkid, outSR: outSR.wkid, extentOfInterest: JSON.stringify(extent), }; (0, arcgisRestJS_1.request)((0, generalHelpers_1.checkUrlPathTermination)(geometryServiceUrl) + "findTransformations", _requestOptions).then((response) => { const transformations = response && response.transformations ? response.transformations : undefined; let transformation; if (transformations && transformations.length > 0) { // if a forward single transformation is found use that...otherwise check for and use composite transformation = transformations[0].wkid ? transformations[0].wkid : transformations[0].geoTransforms ? transformations[0] : undefined; } _requestOptions.params = { f: "json", outSR: outSR.wkid, inSR: extent.spatialReference.wkid, geometries: { geometryType: "esriGeometryPoint", geometries: [ { x: extent.xmin, y: extent.ymin }, { x: extent.xmax, y: extent.ymax }, ], }, transformation: transformation, }; (0, arcgisRestJS_1.request)((0, generalHelpers_1.checkUrlPathTermination)(geometryServiceUrl) + "project", _requestOptions).then((projectResponse) => { const projectGeom = projectResponse.geometries.length === 2 ? projectResponse.geometries : undefined; if (projectGeom) { resolve({ xmin: projectGeom[0].x, ymin: projectGeom[0].y, xmax: projectGeom[1].x, ymax: projectGeom[1].y, spatialReference: outSR, }); } else { resolve(undefined); } }, (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); } }); } exports.convertExtent = convertExtent; /** * Publishes a feature service as an AGOL item; it does not include its layers and tables * * @param newItemTemplate Template of item to be created * @param authentication Credentials for the request * @param templateDictionary Hash of facts: org URL, adlib replacements, user; .user.folders property contains a list * @returns A promise that will resolve with an object reporting success and the Solution id */ function createFeatureService(newItemTemplate, authentication, templateDictionary) { return new Promise((resolve, reject) => { // Create item _getCreateServiceOptions(newItemTemplate, authentication, templateDictionary).then((createOptions) => { (0, arcgisRestJS_1.svcAdminCreateFeatureService)(createOptions).then((createResponse) => { // Federated servers may have inconsistent casing, so lowerCase it createResponse.encodedServiceURL = _lowercaseDomain(createResponse.encodedServiceURL); createResponse.serviceurl = _lowercaseDomain(createResponse.serviceurl); resolve(createResponse); }, (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.createFeatureService = createFeatureService; /** * Publishes an item and its data, metadata, and resources as an AGOL item. * * @param itemInfo Item's `item` section * @param folderId Id of folder to receive item; null indicates that the item goes into the root * folder; ignored for Group item type * @param destinationAuthentication Credentials for for requests to where the item is to be created * @param itemThumbnailUrl URL to image to use for item thumbnail * @param itemThumbnailAuthentication Credentials for requests to the thumbnail source * @param dataFile Item's `data` section * @param metadataFile Item's metadata file * @param resourcesFiles Item's resources * @param access Access to set for item: "public", "org", "private" * @returns A promise that will resolve with an object reporting success or failure and the Solution id */ function createFullItem(itemInfo, folderId, destinationAuthentication, itemThumbnailUrl, itemThumbnailAuthentication, dataFile, metadataFile, resourcesFiles, access = "private") { return new Promise((resolve, reject) => { // Create item const createOptions = { item: { ...itemInfo, }, folderId, authentication: destinationAuthentication, }; // eslint-disable-next-line @typescript-eslint/no-floating-promises addTokenToUrl(itemThumbnailUrl, itemThumbnailAuthentication).then((updatedThumbnailUrl) => { /* istanbul ignore else */ if (updatedThumbnailUrl) { createOptions.item.thumbnailUrl = (0, generalHelpers_1.appendQueryParam)(updatedThumbnailUrl, "w=400"); } (0, arcgisRestJS_1.createItemInFolder)(createOptions).then((createResponse) => { if (createResponse.success) { let accessDef; // Set access if it is not AGOL default // Set the access manually since the access value in createItem appears to be ignored // Need to run serially; will not work reliably if done in parallel with adding the data section if (access !== "private") { const accessOptions = { id: createResponse.id, access: access === "public" ? "public" : "org", authentication: destinationAuthentication, }; accessDef = (0, arcgisRestJS_1.setItemAccess)(accessOptions); } else { accessDef = Promise.resolve({ itemId: createResponse.id, }); } // Now add attached items accessDef.then(() => { const updateDefs = []; // Add the data section if (dataFile) { updateDefs.push(_addItemDataFile(createResponse.id, dataFile, destinationAuthentication)); } // Add the resources via a zip because AGO sometimes loses resources if many are added at the // same time to the same item if (Array.isArray(resourcesFiles) && resourcesFiles.length > 0) { updateDefs.push(new Promise((rsrcResolve, rsrcReject) => { (0, libConnectors_1.createZip)("resources.zip", resourcesFiles).then((zipfile) => { const addResourceOptions = { id: createResponse.id, resource: zipfile, authentication: destinationAuthentication, params: { archive: true, }, }; (0, arcgisRestJS_1.addItemResource)(addResourceOptions).then(rsrcResolve, rsrcReject); }, rsrcReject); })); } // Add the metadata section if (metadataFile) { updateDefs.push(_addItemMetadataFile(createResponse.id, metadataFile, destinationAuthentication)); } // Wait until all adds are done Promise.all(updateDefs).then(() => resolve(createResponse), (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else { reject((0, generalHelpers_1.fail)()); } }, (e) => reject((0, generalHelpers_1.fail)(e))); }); }); } exports.createFullItem = createFullItem; /** * Publishes an item and its data as an AGOL item. * * @param itemInfo Item's `item` section * @param dataInfo Item's `data` section * @param authentication Credentials for the request * @param folderId Id of folder to receive item; null indicates that the item goes into the root * folder; ignored for Group item type * @param access Access to set for item: "public", "org", "private" * @returns A promise that will resolve with an object reporting success and the Solution id */ function createItemWithData(itemInfo, dataInfo, authentication, folderId, access = "private") { return new Promise((resolve, reject) => { // Create item const createOptions = { item: { title: "_", ...itemInfo, data: dataInfo, }, folderId, authentication: authentication, }; if (itemInfo.thumbnail) { createOptions.params = { // Pass thumbnail file in via params because item property is serialized, which discards a blob thumbnail: itemInfo.thumbnail, }; delete createOptions.item.thumbnail; } if (createOptions.params) { if (dataInfo instanceof File) { createOptions.params["file"] = dataInfo; } else { createOptions.params["text"] = dataInfo; } } else { if (dataInfo instanceof File) { createOptions.params = { file: dataInfo, }; } else { createOptions.params = { text: dataInfo ? dataInfo : {}, }; } } (0, arcgisRestJS_1.createItemInFolder)(createOptions).then((createResponse) => { if (createResponse.success) { if (access !== "private") { // Set access if it is not AGOL default // Set the access manually since the access value in createItem appears to be ignored const accessOptions = { id: createResponse.id, access: access === "public" ? "public" : "org", authentication: authentication, }; (0, arcgisRestJS_1.setItemAccess)(accessOptions).then(() => { resolve({ folder: createResponse.folder, id: createResponse.id, success: true, }); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else { resolve({ folder: createResponse.folder, id: createResponse.id, success: true, }); } } else { reject((0, generalHelpers_1.fail)()); } }, (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.createItemWithData = createItemWithData; /** * Creates a folder using a numeric suffix to ensure uniqueness if necessary. * * @param title Folder title, used as-is if possible and with suffix otherwise * @param templateDictionary Hash of facts: org URL, adlib replacements, user; .user.folders property contains a list * of known folder names; function updates list with existing names not yet known, and creates .user.folders if it * doesn't exist in the dictionary * @param authentication Credentials for creating folder * @returns Id of created folder */ function createUniqueFolder(title, templateDictionary, authentication) { return new Promise((resolve, reject) => { // Get a title that is not already in use const folderTitle = (0, generalHelpers_1.getUniqueTitle)(title, templateDictionary, "user.folders"); const folderCreationParam = { title: folderTitle, authentication: authentication, }; (0, arcgisRestJS_1.createFolder)(folderCreationParam).then((ok) => resolve(ok), (err) => { // If the name already exists, we'll try again const errorDetails = (0, generalHelpers_1.getProp)(err, "response.error.details"); if (Array.isArray(errorDetails) && errorDetails.length > 0) { const nameNotAvailMsg = "Folder title '" + folderTitle + "' not available."; if (errorDetails.indexOf(nameNotAvailMsg) >= 0) { // Create the user.folders property if it doesn't exist /* istanbul ignore else */ if (!(0, generalHelpers_1.getProp)(templateDictionary, "user.folders")) { (0, generalHelpers_1.setCreateProp)(templateDictionary, "user.folders", []); } templateDictionary.user.folders.push({ title: folderTitle, }); createUniqueFolder(title, templateDictionary, authentication).then(resolve, reject); } else { reject(err); } } else { // Otherwise, error out reject(err); } }); }); } exports.createUniqueFolder = createUniqueFolder; /** * Creates a group using numeric suffix to ensure uniqueness. * * @param title Group title, used as-is if possible and with suffix otherwise * @param templateDictionary Hash of facts: org URL, adlib replacements, user * @param authentication Credentials for creating group * @param owner Optional arg for the Tracking owner * If the tracking owner is not the one deploying the solution * tracker groups will be created under the deployment user but * will be reassigned to the tracking owner. * @returns Information about created group */ function createUniqueGroup(title, groupItem, templateDictionary, authentication, owner) { return new Promise((resolve, reject) => { let groupsPromise; // when working with tracker we need to consider the groups of the deploying user as well as the groups // of the tracking user...must be unique for both if (owner && owner !== authentication.username) { groupsPromise = searchAllGroups(`(owner:${owner}) orgid:${templateDictionary.organization.id}`, authentication); } else { groupsPromise = Promise.resolve([]); } // first get the tracker owner groups groupsPromise.then((groups) => { templateDictionary["allGroups"] = groups.concat((0, generalHelpers_1.getProp)(templateDictionary, "user.groups")); // Get a title that is not already in use groupItem.title = (0, generalHelpers_1.getUniqueTitle)(title, templateDictionary, "allGroups"); const groupCreationParam = { group: groupItem, authentication: authentication, }; (0, arcgisRestJS_1.createGroup)(groupCreationParam).then(resolve, (err) => { // If the name already exists, we'll try again const errorDetails = (0, generalHelpers_1.getProp)(err, "response.error.details"); if (Array.isArray(errorDetails) && errorDetails.length > 0) { const nameNotAvailMsg = "You already have a group named '" + groupItem.title + "'. Try a different name."; if (errorDetails.indexOf(nameNotAvailMsg) >= 0) { templateDictionary.user.groups.push({ title: groupItem.title, }); createUniqueGroup(title, groupItem, templateDictionary, authentication).then(resolve, reject); } else { reject(err); } } else { // Otherwise, error out reject(err); } }); }, (e) => reject(e)); }); } exports.createUniqueGroup = createUniqueGroup; /** * Gets the ids of the dependencies of an AGOL feature service item. * Dependencies will only exist when the service is a view. * * @param itemTemplate Template of item to be created * @param authentication Credentials for the request * @returns A promise that will resolve a list of dependencies */ function extractDependencies(itemTemplate, authentication) { const dependencies = []; return new Promise((resolve, reject) => { // Get service dependencies when the item is a view // This step is skipped for tracker views as they will already have a source service in the org if (itemTemplate.properties.service.isView && itemTemplate.item.url && !(0, trackingHelpers_1.isTrackingViewTemplate)(itemTemplate)) { (0, arcgisRestJS_1.request)((0, generalHelpers_1.checkUrlPathTermination)(itemTemplate.item.url) + "sources?f=json", { authentication: authentication, }).then((response) => { /* istanbul ignore else */ if (response && response.services) { response.services.forEach((layer) => { dependencies.push({ id: layer.serviceItemId, name: layer.name, }); }); } resolve(dependencies); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else if ((0, workforceHelpers_1.isWorkforceProject)(itemTemplate)) { resolve((0, workforceHelpers_1.getWorkforceDependencies)(itemTemplate, dependencies)); } else { resolve(dependencies); } }); } exports.extractDependencies = extractDependencies; /** * Get json info for the services layers * * @param serviceUrl the url for the service * @param layerList list of base layer info * @param authentication Credentials for the request * @returns A promise that will resolve a list of dependencies */ function getLayers(serviceUrl, layerList, authentication) { return new Promise((resolve, reject) => { if (layerList.length === 0) { resolve([]); } // get the admin URL serviceUrl = serviceUrl.replace("/rest/services", "/rest/admin/services"); const requestsDfd = []; layerList.forEach((layer) => { const requestOptions = { authentication: authentication, }; requestsDfd.push((0, arcgisRestJS_1.request)((0, generalHelpers_1.checkUrlPathTermination)(serviceUrl) + layer["id"] + "?f=json", requestOptions)); }); // Wait until all layers are heard from Promise.all(requestsDfd).then((layers) => resolve(layers), (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.getLayers = getLayers; /** * Add additional options to a layers definition. * * @param args The IPostProcessArgs for the request(s) * @param isPortal boolean to indicate if we are deploying to portal * * @returns An array of update instructions * @private */ function getLayerUpdates(args, isPortal) { const adminUrl = args.itemTemplate.item.url.replace("rest/services", "rest/admin/services"); const updates = []; const refresh = _getUpdate(adminUrl, null, null, args, "refresh"); updates.push(refresh); Object.keys(args.objects).forEach((id) => { const obj = Object.assign({}, args.objects[id]); // These properties cannot be set in the update definition when working with portal if (isPortal) { (0, generalHelpers_1.deleteProps)(obj, ["type", "id", "relationships", "sourceServiceFields"]); } // handle definition deletes // removes previous editFieldsInfo fields if their names were changed if (obj.hasOwnProperty("deleteFields")) { updates.push(_getUpdate(adminUrl, id, obj, args, "delete")); (0, generalHelpers_1.deleteProp)(obj, "deleteFields"); updates.push(_getUpdate(adminUrl, null, null, args, "refresh")); } }); const subtypeUpdates = _getSubtypeUpdates({ message: "add subtype updates", objects: args.objects, itemTemplate: args.itemTemplate, authentication: args.authentication, }); /* istanbul ignore else */ if (subtypeUpdates.length > 0 && isPortal) { subtypeUpdates.forEach((subtypeUpdate) => { updates.push(_getUpdate(adminUrl + subtypeUpdate.id, null, { subtypeField: subtypeUpdate.subtypeField }, args, "update")); updates.push(_getUpdate(adminUrl + subtypeUpdate.id, null, { defaultSubtypeCode: subtypeUpdate.defaultSubtypeCode }, args, "update")); updates.push(_getUpdate(adminUrl + subtypeUpdate.id, null, { subtypes: subtypeUpdate.subtypes }, args, "add")); }); } // issue: #706 // Add source service relationships // views will now always add all layers in a single call and will inherit the relationships from the source service if (!args.itemTemplate.properties.service.isView) { const relUpdates = _getRelationshipUpdates({ message: "updated layer relationships", objects: args.objects, itemTemplate: args.itemTemplate, authentication: args.authentication, }); // issue: #724 // In portal the order the relationships are added needs to follow the layer order // otherwise the relationship IDs will be reset relUpdates.layers = _sortRelationships(args.itemTemplate.properties.layers, args.itemTemplate.properties.tables, relUpdates); /* istanbul ignore else */ if (relUpdates.layers.length > 0) { updates.push(_getUpdate(adminUrl, null, relUpdates, args, "add")); updates.push(refresh); } // handle contingent values const contingentValuesUpdates = _getContingentValuesUpdates({ message: "add layer contingent values", objects: args.objects, itemTemplate: args.itemTemplate, authentication: args.authentication, }); /* istanbul ignore else */ if (contingentValuesUpdates.length > 0) { contingentValuesUpdates.forEach((conUpdate) => { updates.push(_getUpdate(adminUrl + conUpdate.id, null, conUpdate.contingentValues, args, "add")); }); } } // issue: https://devtopia.esri.com/WebGIS/solution-deployment-apps/issues/273 // For portal only...add specific indexes with existing supplementary addToDefinition call if it exists // or with a new addToDefinition call if one doesn't already exist if (isPortal) { Object.keys(args.objects).forEach((id) => { const obj = Object.assign({}, args.objects[id]); let update; if (Array.isArray(obj.indexes) && obj.indexes.length > 0) { const layerHasExistingAdd = updates.some((u) => { if (u.url.indexOf(`${id}/addToDefinition`) > -1) { update = u; return true; } }); if (layerHasExistingAdd) { // append to existing addToDef update.params.addToDefinition = { ...update.params.addToDefinition, indexes: obj.indexes, }; } else { // create new addToDef updates.push(_getUpdate((0, generalHelpers_1.checkUrlPathTermination)(adminUrl) + id, null, { indexes: obj.indexes }, args, "add")); } } }); } return updates.length === 1 ? [] : updates; } exports.getLayerUpdates = getLayerUpdates; /** * Moves an AGO item to a specified folder. * * @param itemId Id of item to move * @param folderId Id of folder to receive item * @param authentication Credentials for the request * @returns A Promise resolving to the results of the move */ async function moveItemToFolder(itemId, folderId, authentication) { const moveOptions = { itemId, folderId, authentication, }; return (0, arcgisRestJS_1.moveItem)(moveOptions); } exports.moveItemToFolder = moveItemToFolder; /** * Moves a list of AGO items to a specified folder. * * @param itemIds Ids of items to move * @param folderId Id of folder to receive item * @param authentication Credentials for the request * @returns A Promise resolving to the results of the moves */ async function moveItemsToFolder(itemIds, folderId, authentication) { const movePromises = new Array(); itemIds.forEach((itemId) => { movePromises.push(moveItemToFolder(itemId, folderId, authentication)); }); return Promise.all(movePromises); } exports.moveItemsToFolder = moveItemsToFolder; /** * Sorts relationships based on order of supporting layers and tables in the service definition * * @param layers the layers from the service * @param tables the tables from the service * @param relUpdates the relationships to add for the service * * @returns An array with relationships that have been sorted * @private */ function _sortRelationships(layers, tables, relUpdates) { const ids = [].concat(layers.map((l) => l.id), tables.map((t) => t.id)); // In portal the order the relationships are added needs to follow the layer order // otherwise the relationship IDs will be reset const _relUpdateLayers = []; ids.forEach((id) => { relUpdates.layers.some((relUpdate) => { if (id === relUpdate.id) { _relUpdateLayers.push(relUpdate); return true; } else { return false; } }); }); return _relUpdateLayers; } exports._sortRelationships = _sortRelationships; /** * Add additional options to a layers definition * * Added retry due to some solutions failing to deploy in specific orgs/hives * * * @param Update will contain either add, update, or delete from service definition call * @param skipRetry defaults to false. when true the retry logic will be ignored * @returns A promise that will resolve when service definition call has completed * @private */ /* istanbul ignore else */ function getRequest(update, skipRetry = false, useAsync = false, isPortal = false) { return new Promise((resolve, reject) => { const options = { params: update.params, authentication: update.args.authentication, }; /* istanbul ignore else */ if ((useAsync && update.url.indexOf("addToDefinition") > -1) || update.url.indexOf("updateDefinition") > -1 || update.url.indexOf("deleteFromDefinition") > -1) { options.params = { ...options.params, async: true }; } (0, arcgisRestJS_1.request)(update.url, options).then((result) => { checkRequestStatus(result, options.authentication).then(() => resolve(null), (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => { if (!skipRetry) { getRequest(update, true, true, isPortal).then(() => resolve(), (e) => reject(e)); } else { reject(e); } }); }); } exports.getRequest = getRequest; /** * Fills in missing data, including full layer and table definitions, in a feature services' definition. * * @param itemTemplate Feature service item, data, dependencies definition to be modified * @param authentication Credentials for the request to AGOL * @returns A promise that will resolve when fullItem has been updated * @private */ function getServiceLayersAndTables(itemTemplate, authentication) { return new Promise((resolve, reject) => { // To have enough information for reconstructing the service, we'll supplement // the item and data sections with sections for the service, full layers, and // full tables // Extra steps must be taken for workforce version 2 const isWorkforceService = (0, workforceHelpers_1.isWorkforceProject)(itemTemplate); // Get the service description if (itemTemplate.item.url) { getFeatureServiceProperties(itemTemplate.item.url, authentication, isWorkforceService).then((properties) => { itemTemplate.properties = properties; resolve(itemTemplate); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else { resolve(itemTemplate); } }); } exports.getServiceLayersAndTables = getServiceLayersAndTables; /** * Get service properties for the given url and update key props * * @param serviceUrl the feature service url * @param authentication Credentials for the request to AGOL * @param workforceService boolean to indicate if extra workforce service steps should be handled * @returns A promise that will resolve with the service properties * @private */ function getFeatureServiceProperties(serviceUrl, authentication, workforceService = false) { return new Promise((resolve, reject) => { const properties = { service: {}, layers: [], tables: [], }; // get the admin URL serviceUrl = serviceUrl.replace("/rest/services", "/rest/admin/services"); // Get the service description (0, arcgisRestJS_1.request)(serviceUrl + "?f=json", { authentication: authentication, }).then((serviceData) => { properties.service = _parseAdminServiceData(serviceData); // Copy cacheMaxAge to top level so that AGO sees it when deploying the service // serviceData may have set it if there isn't an adminServiceInfo /* istanbul ignore else */ if (serviceData.adminServiceInfo?.cacheMaxAge) { properties.service.cacheMaxAge = serviceData.adminServiceInfo.cacheMaxAge; } // Move the layers and tables out of the service's data section /* istanbul ignore else */ if (serviceData.layers) { properties.layers = serviceData.layers; // Fill in properties that the service layer doesn't provide // and remove properties that should not exist in the template properties.layers.forEach((layer) => { layer.serviceItemId = properties.service.serviceItemId; layer.extent = null; (0, featureServiceHelpers_1.removeLayerOptimization)(layer); }); } delete serviceData.layers; /* istanbul ignore else */ if (serviceData.tables) { properties.tables = serviceData.tables; // Fill in properties that the service layer doesn't provide properties.tables.forEach((table) => { table.serviceItemId = properties.service.serviceItemId; table.extent = null; }); } delete serviceData.tables; // Ensure solution items have unique indexes on relationship key fields _updateIndexesForRelationshipKeyFields(properties); (0, featureServiceHelpers_1.processContingentValues)(properties, serviceUrl, authentication).then(() => { if (workforceService) { (0, workforceHelpers_1.getWorkforceServiceInfo)(properties, serviceUrl, authentication).then(resolve, reject); } else { resolve(properties); } }, (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.getFeatureServiceProperties = getFeatureServiceProperties; /** * Fetches the configuration of a workflow. * * @param itemId Id of the workflow item * @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId" * @param authentication Credentials for the request to AGOL * @returns Promise resolving with the workflow configuration in a zip file * @throws {WorkflowJsonExceptionDTO} if request to workflow manager fails */ async function getWorkflowConfigurationZip(itemId, workflowBaseUrl, authentication) { const url = `${workflowBaseUrl}/admin/${itemId}/export`; return (0, arcgisRestJS_1.request)(url, { authentication, headers: { "Accept": "application/octet-stream", "Authorization": `Bearer ${authentication.token}`, "X-Esri-Authorization": `Bearer ${authentication.token}`, }, params: { f: "zip", }, }); } exports.getWorkflowConfigurationZip = getWorkflowConfigurationZip; /** * Sets the configuration of a workflow. * * @param itemId Id of the workflow item * @param configurationZipFile Configuration files in a zip file * @param workflowBaseUrl URL of the workflow manager, e.g., "https://workflow.arcgis.com/orgId" * @param authentication Credentials for the request to AGOL * @returns Promise resolving with the workflow configuration in a zip file * @throws {WorkflowJsonExceptionDTO} if request to workflow manager fails */ async function setWorkflowConfigurationZip(itemId, configurationZipFile, workflowBaseUrl, authentication) { const url = `${workflowBaseUrl}/admin/${itemId}/import`; return (0, arcgisRestJS_1.request)(url, { authentication, headers: { "Accept": "application/octet-stream", "Authorization": `Bearer ${authentication.token}`, "X-Esri-Authorization": `Bearer ${authentication.token}`, }, params: { file: configurationZipFile, }, }); } exports.setWorkflowConfigurationZip = setWorkflowConfigurationZip; /** * Parses the layers array and will filter subsets of Layers and Tables * Layers and Tables are both returned in the layers array when we access a featu