@esri/solution-common
Version:
Provides general helper functions for @esri/solution.js.
1,087 lines • 89.8 kB
JavaScript
"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