@esri/solution-common
Version:
Provides general helper functions for @esri/solution.js.
691 lines • 28 kB
JavaScript
/** @license
* Copyright 2020 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.
*/
/**
* Provides general helper functions.
*
* @module workforceHelpers
*/
import { applyEdits, queryFeatures, request } from "./arcgisRestJS";
import { getIDs, getProp, fail, idTest, regExTest, setProp } from "./generalHelpers";
import { templatizeTerm, replaceInTemplate } from "./templatization";
import { cacheLayerInfo } from "./featureServiceHelpers";
//#region workforce v1
/**
* Converts an workforce item to a template.
*
* @param itemTemplate template for the workforce project item
* @param authentication credentials for any requests
* @param templateDictionary Hash of key details used for variable replacement
* @returns templatized itemTemplate
*/
export function convertWorkforceItemToTemplate(itemTemplate, authentication, templateDictionary) {
return new Promise((resolve, reject) => {
// This function is specific to workforce v1 project structure
// Key properties that contain item IDs for the workforce project type
const keyProperties = getKeyWorkforceProperties(1);
// The templates data to process
const data = itemTemplate.data;
if (data) {
// Extract dependencies
extractWorkforceDependencies(data, keyProperties, authentication).then((results) => {
itemTemplate.dependencies = results.dependencies;
// templatize key properties
itemTemplate.data = templatizeWorkforce(data, keyProperties, results.urlHash, templateDictionary);
resolve(itemTemplate);
}, (e) => reject(fail(e)));
}
else {
resolve(itemTemplate);
}
});
}
/**
* Gets the ids of the dependencies of the workforce project.
*
* @param data itemTemplate data
* @param keyProperties workforce project properties that contain references to dependencies
* @param authentication credentials for any requests
* @returns List of dependencies ids
*/
export function extractWorkforceDependencies(data, keyProperties, authentication) {
return new Promise((resolve, reject) => {
const deps = [];
// get the ids for the service dependencies
// "workerWebMapId" and "dispatcherWebMapId" are already IDs and don't have a serviceItemId
keyProperties.forEach((p) => {
const serviceItemId = getProp(data, p + ".serviceItemId");
const v = getProp(data, p);
if (serviceItemId) {
if (deps.indexOf(serviceItemId) === -1) {
deps.push(serviceItemId);
}
}
else {
idTest(v, deps);
}
});
if (getProp(data, "assignmentIntegrations")) {
let requests = [];
let urls = [];
data.assignmentIntegrations.forEach((ai) => {
if (ai.assignmentTypes) {
const assignmentKeys = Object.keys(ai.assignmentTypes);
assignmentKeys.forEach((k) => {
const urlTemplate = ai.assignmentTypes[k].urlTemplate;
idTest(urlTemplate, deps);
const serviceRequests = urlTest(urlTemplate, authentication);
if (Array.isArray(serviceRequests.requests) && serviceRequests.requests.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
requests = requests.concat(serviceRequests.requests);
urls = urls.concat(serviceRequests.urls);
}
});
}
});
if (requests.length > 0) {
Promise.all(requests).then((results) => {
const urlHash = {};
// Get the serviceItemId for the url
/* istanbul ignore else */
if (Array.isArray(results)) {
results.forEach((result, i) => {
/* istanbul ignore else */
if (result.serviceItemId) {
urlHash[urls[i]] = result.serviceItemId;
/* istanbul ignore else */
if (deps.indexOf(result.serviceItemId) === -1) {
deps.push(result.serviceItemId);
}
}
});
}
resolve({
dependencies: deps,
urlHash: urlHash,
});
}, (e) => reject(fail(e)));
}
else {
resolve({
dependencies: deps,
urlHash: {},
});
}
}
else {
resolve({
dependencies: deps,
urlHash: {},
});
}
});
}
/**
* Templatizes key item properties.
*
* @param data itemTemplate data
* @param keyProperties workforce project properties that should be templatized
* @param urlHash a key value pair of url and itemId
* @param templateDictionary Hash of key details used for variable replacement
* @returns an updated data object to be stored in the template
*/
export function templatizeWorkforce(data, keyProperties, urlHash, templateDictionary) {
keyProperties.forEach((p) => {
/* istanbul ignore else */
if (getProp(data, p)) {
if (getProp(data[p], "serviceItemId")) {
// templatize properties with id and url
const id = data[p].serviceItemId;
let serviceItemIdSuffix = ".itemId";
/* istanbul ignore else */
if (getProp(data[p], "url")) {
const layerId = getLayerId(data[p].url);
cacheLayerInfo(layerId, id, data[p].url, templateDictionary);
data[p].url = templatizeTerm(id, id, getReplaceValue(layerId, ".url"));
serviceItemIdSuffix = getReplaceValue(layerId, serviceItemIdSuffix);
}
data[p].serviceItemId = templatizeTerm(id, id, serviceItemIdSuffix);
}
else {
// templatize simple id properties
data[p] = templatizeTerm(data[p], data[p], ".itemId");
}
}
});
data["folderId"] = "{{folderId}}";
// templatize app integrations
const integrations = data.assignmentIntegrations || [];
integrations.forEach((i) => {
_templatizeUrlTemplate(i, urlHash);
/* istanbul ignore else */
if (i.assignmentTypes) {
const assignmentKeys = Object.keys(i.assignmentTypes);
assignmentKeys.forEach((k) => {
_templatizeUrlTemplate(i.assignmentTypes[k], urlHash);
});
}
});
return data;
}
//#endregion
/**
* Evaluate key properties in the workforce service of additional items that are needed
*
* @param itemTemplate template for the workforce project item
* @param dependencies list of the items dependencies
* @returns an array of objects with dependency ids
*/
export function getWorkforceDependencies(itemTemplate, dependencies) {
const properties = itemTemplate.item.properties || {};
const keyProperties = getKeyWorkforceProperties(2);
dependencies = keyProperties.reduce(function (acc, v) {
/* istanbul ignore else */
if (properties[v] && dependencies.indexOf(properties[v]) < 0) {
acc.push(properties[v]);
}
return acc;
}, dependencies);
// We also need the dependencies listed in the Assignment Integrations table
const infos = getProp(itemTemplate, "properties.workforceInfos.assignmentIntegrationInfos");
/* istanbul ignore else */
if (infos && infos.length > 0) {
infos.forEach((info) => {
const infoKeys = Object.keys(info);
/* istanbul ignore else */
if (infoKeys.indexOf("dependencies") > -1) {
info["dependencies"].forEach((d) => {
/* istanbul ignore else */
if (dependencies.indexOf(d) < 0) {
dependencies.push(d);
}
});
}
});
}
return dependencies.map((d) => {
return { id: d, name: "" };
});
}
export function getWorkforceServiceInfo(properties, url, authentication) {
return new Promise((resolve, reject) => {
url = url.replace("/rest/admin/services", "/rest/services");
url = _updateUrl(url);
const requests = [
queryFeatures({
url: `${url}3`,
where: "1=1",
authentication,
}),
queryFeatures({
url: `${url}4`,
where: "1=1",
authentication,
}),
];
Promise.all(requests).then((responses) => {
const [assignmentTypes, assignmentIntegrations] = responses;
properties.workforceInfos = {
assignmentTypeInfos: _getAssignmentTypeInfos(assignmentTypes),
};
_getAssignmentIntegrationInfos(assignmentIntegrations, authentication).then((results) => {
properties.workforceInfos["assignmentIntegrationInfos"] = results;
_updateGlobalIdAndAssignmentType(properties.workforceInfos);
resolve(properties);
}, (e) => reject(fail(e)));
}, (e) => reject(fail(e)));
});
}
/**
* Wrap global id and assignmenttype values in curly braces
*
* Added for issue #734
*
* This function will update the provided workforceInfos object
*
* @param workforceInfos the object that stores the integration and type info with global ids
* @private
*/
export function _updateGlobalIdAndAssignmentType(workforceInfos) {
const updateId = (i) => {
/* istanbul ignore else */
if (i["GlobalID"]) {
i["GlobalID"] = `{${i["GlobalID"]}}`;
}
/* istanbul ignore else */
if (i["assignmenttype"]) {
i["assignmenttype"] = `{${i["assignmenttype"]}}`;
}
return i;
};
const assignmentIntegrationInfos = getProp(workforceInfos, "assignmentIntegrationInfos");
/* istanbul ignore else */
if (assignmentIntegrationInfos && Array.isArray(assignmentIntegrationInfos)) {
setProp(workforceInfos, "assignmentIntegrationInfos", assignmentIntegrationInfos.map(updateId));
}
const assignmentTypeInfos = getProp(workforceInfos, "assignmentTypeInfos");
/* istanbul ignore else */
if (assignmentTypeInfos && Array.isArray(assignmentTypeInfos)) {
setProp(workforceInfos, "assignmentTypeInfos", assignmentTypeInfos.map(updateId));
}
}
//TODO: function doc
export function _getAssignmentTypeInfos(assignmentTypes) {
// Assignment Types
const assignmentTypeInfos = [];
const keyAssignmentTypeProps = ["description", assignmentTypes.globalIdFieldName];
assignmentTypes.features.forEach((f) => {
const info = {};
keyAssignmentTypeProps.forEach((p) => {
info[p] = f.attributes[p];
});
assignmentTypeInfos.push(info);
});
return assignmentTypeInfos;
}
//TODO: function doc
export function _getAssignmentIntegrationInfos(assignmentIntegrations, authentication) {
return new Promise((resolve, reject) => {
let requests = [];
let urls = [];
const assignmentIntegrationInfos = [];
const keyAssignmentIntegrationsProps = [
"appid",
assignmentIntegrations.globalIdFieldName,
"prompt",
"urltemplate",
"assignmenttype",
];
assignmentIntegrations.features.forEach((f) => {
const info = {};
keyAssignmentIntegrationsProps.forEach((p) => {
info[p] = f.attributes[p];
/* istanbul ignore else */
if (p === "urltemplate") {
const urlTemplate = f.attributes[p];
const ids = getIDs(urlTemplate);
info["dependencies"] = ids;
const serviceRequests = urlTest(urlTemplate, authentication);
/* istanbul ignore else */
if (Array.isArray(serviceRequests.requests) && serviceRequests.requests.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
requests = requests.concat(serviceRequests.requests);
urls = urls.concat(serviceRequests.urls);
}
}
});
assignmentIntegrationInfos.push(info);
});
getUrlDependencies(requests, urls).then((results) => {
assignmentIntegrationInfos.forEach((ai) => {
_templatizeUrlTemplate(ai, results.urlHash);
});
resolve(assignmentIntegrationInfos);
}, (e) => reject(fail(e)));
});
}
export function getUrlDependencies(requests, urls) {
return new Promise((resolve, reject) => {
const dependencies = [];
if (requests.length > 0) {
Promise.all(requests).then((results) => {
const urlHash = {};
// Get the serviceItemId for the url
/* istanbul ignore else */
if (Array.isArray(results)) {
results.forEach((result, i) => {
/* istanbul ignore else */
if (result.serviceItemId) {
urlHash[urls[i]] = result.serviceItemId;
/* istanbul ignore else */
if (dependencies.indexOf(result.serviceItemId) === -1) {
dependencies.push(result.serviceItemId);
}
}
});
}
resolve({
dependencies,
urlHash,
});
}, (e) => reject(fail(e)));
}
else {
resolve({
dependencies,
urlHash: {},
});
}
});
}
/**
* Templatizes values from a urlTemplate
*
* @param item the object that may contain a urlTemplate
* @param urlHash a key value pair of url and itemId
* @private
*/
export function _templatizeUrlTemplate(item, urlHash) {
// v1 uses urlTemplate
// v2 uses urltemplate
const urlTemplateVar = getProp(item, "urlTemplate") ? "urlTemplate" : "urltemplate";
let urlTemplate = getProp(item, urlTemplateVar);
/* istanbul ignore else */
if (urlTemplate) {
const ids = getIDs(urlTemplate);
ids.forEach((id) => {
urlTemplate = urlTemplate.replace(id, templatizeTerm(id, id, ".itemId"));
});
const urls = _getURLs(urlTemplate);
urls.forEach((url) => {
const layerId = getLayerId(url);
const replaceValue = getReplaceValue(layerId, ".url");
const id = urlHash[url];
/* istanbul ignore else */
if (Array.isArray(item.dependencies) && item.dependencies.indexOf(id) < 0) {
item.dependencies.push(id);
}
urlTemplate = urlTemplate.replace(url, templatizeTerm(id, id, replaceValue));
});
setProp(item, urlTemplateVar, urlTemplate);
}
}
export function getLayerId(url) {
return url.indexOf("FeatureServer/") > -1 ? url.substr(url.lastIndexOf("/") + 1) : undefined;
}
export function getReplaceValue(layerId, suffix) {
return isNaN(Number.parseInt(layerId, 10)) ? `${suffix}` : `.layer${layerId}${suffix}`;
}
export function postProcessWorkforceTemplates(templates) {
const groupUpdates = {};
const _templates = templates.map((t) => {
// templatize Workforce Project
t = _templatizeWorkforceProject(t, groupUpdates);
// templatize Workforce Dispatcher
t = _templatizeWorkforceDispatcherOrWorker(t, "Workforce Dispatcher");
// templatize Workforce Worker
t = _templatizeWorkforceDispatcherOrWorker(t, "Workforce Worker");
return t;
});
return _templates.map((t) => {
if (groupUpdates[t.itemId]) {
t.dependencies = t.dependencies.concat(groupUpdates[t.itemId]);
}
return t;
});
}
//TODO: function doc
export function _templatizeWorkforceProject(t, groupUpdates) {
/* istanbul ignore else */
if (isWorkforceProject(t)) {
const properties = t.item.properties || {};
const keyProperties = getKeyWorkforceProperties(2);
const groupId = properties["workforceProjectGroupId"];
const shuffleIds = [];
Object.keys(properties).forEach((p) => {
/* istanbul ignore else */
if (keyProperties.indexOf(p) > -1) {
const id = properties[p];
/* istanbul ignore else */
if (id !== groupId) {
shuffleIds.push(id);
}
t.item.properties[p] = templatizeTerm(properties[p], properties[p], ".itemId");
}
});
// update the dependencies
t.dependencies = t.dependencies.filter((d) => d !== groupId && shuffleIds.indexOf(d) < 0);
// shuffle and cleanup
const workforceInfos = getProp(t, "properties.workforceInfos");
/* istanbul ignore else */
if (workforceInfos) {
Object.keys(workforceInfos).forEach((k) => {
workforceInfos[k].forEach((wInfo) => {
/* istanbul ignore else */
if (wInfo.dependencies) {
wInfo.dependencies.forEach((id) => {
/* istanbul ignore else */
if (shuffleIds.indexOf(id) < 0) {
shuffleIds.push(id);
}
const depIndex = t.dependencies.indexOf(id);
/* istanbul ignore else */
if (depIndex > -1) {
t.dependencies.splice(depIndex, 1);
}
});
delete wInfo.dependencies;
}
});
});
}
// move the dependencies to the group
groupUpdates[groupId] = shuffleIds;
}
return t;
}
//TODO: function doc
export function _templatizeWorkforceDispatcherOrWorker(t, type) {
/* istanbul ignore else */
if ((t.item.typeKeywords || []).indexOf(type) > -1) {
const properties = t.item.properties || {};
const fsId = properties["workforceFeatureServiceId"];
/* istanbul ignore else */
if (fsId) {
t.item.properties["workforceFeatureServiceId"] = templatizeTerm(fsId, fsId, ".itemId");
}
}
return t;
}
// Helpers
export function isWorkforceProject(itemTemplate) {
return (itemTemplate.item.typeKeywords || []).indexOf("Workforce Project") > -1;
}
export function getKeyWorkforceProperties(version) {
return version === 1
? ["groupId", "workerWebMapId", "dispatcherWebMapId", "dispatchers", "assignments", "workers", "tracks"]
: ["workforceDispatcherMapId", "workforceProjectGroupId", "workforceWorkerMapId"];
}
/**
* Test the provided value for any urls and submit a request to obtain the service item id for the url
*
* @param v a string value to test for urls
* @param authentication credentials for the requests
* @returns an object with any pending requests and the urls that requests were made to
*/
export function urlTest(v, authentication) {
const urls = _getURLs(v);
const requests = [];
urls.forEach((url) => {
const options = {
f: "json",
authentication: authentication,
};
requests.push(request(url, options));
});
return {
requests: requests,
urls: urls,
};
}
//TODO: function doc
export function _getURLs(v) {
return regExTest(v, /=(http.*?FeatureServer.*?(?=&|$))/gi).map((_v) => _v.replace("=", ""));
}
//#region Deploy Process ---------------------------------------------------------------------------------------//
/**
* Gets the current user and updates the dispatchers service
*
* @param newlyCreatedItem Item to be created; n.b.: this item is modified
* @param destinationAuthentication The session used to create the new item(s)
* @returns A promise that will resolve with \{ "success" === true || false \}
*/
export function fineTuneCreatedWorkforceItem(newlyCreatedItem, destinationAuthentication, url, templateDictionary) {
return new Promise((resolve, reject) => {
destinationAuthentication.getUser().then((user) => {
// update url with slash if necessary
url = _updateUrl(url);
// Dispatchers...index 2 for workforce v2
// for v1 we need tp fetch from dispatchers for v2 we use the items url
const dispatchers = getProp(newlyCreatedItem, "data.dispatchers");
// add current user as dispatcher
_updateDispatchers(dispatchers && dispatchers.url ? dispatchers.url : `${url}2`, user.username || "", user.fullName || "", destinationAuthentication, templateDictionary.isPortal).then((results) => {
// for workforce v2 we storce the key details from the workforce service as workforceInfos
// now we need to detemplatize it and update the workforce service
let workforceInfos = getProp(newlyCreatedItem, "properties.workforceInfos");
if (workforceInfos && url) {
workforceInfos = replaceInTemplate(workforceInfos, templateDictionary);
_getFields(url, [2, 3, 4], destinationAuthentication).then((fields) => {
// Assignment Types...index 3
const assignmentTypeUrl = `${url}3`;
const assignmentTypeInfos = workforceInfos.assignmentTypeInfos;
const assignmentTypeFeatures = _getAddFeatures(assignmentTypeInfos, fields[assignmentTypeUrl]);
const assignmentTypePromise = _applyEdits(assignmentTypeUrl, assignmentTypeFeatures, destinationAuthentication, true);
// Assignment Integrations...index 4
const assignmentIntegrationUrl = `${url}4`;
const assignmentIntegrationInfos = workforceInfos.assignmentIntegrationInfos;
const assignmentIntegrationFeatures = _getAddFeatures(assignmentIntegrationInfos, fields[assignmentIntegrationUrl]);
const assignmentIntegrationPromise = _applyEdits(assignmentIntegrationUrl, assignmentIntegrationFeatures, destinationAuthentication, true);
Promise.all([assignmentTypePromise, assignmentIntegrationPromise]).then(resolve, reject);
}, (e) => reject(fail(e)));
}
else {
resolve({ success: results });
}
}, (e) => reject(fail(e)));
}, (e) => reject(fail(e)));
});
}
//TODO: function doc
export function _getFields(url, ids, authentication) {
return new Promise((resolve, reject) => {
const options = {
f: "json",
fields: "*",
authentication: authentication,
};
url = _updateUrl(url);
const promises = [];
ids.forEach((id) => {
promises.push(request(`${url}${id}`, options));
});
Promise.all(promises).then((results) => {
const finalResult = {};
results.forEach((r) => {
finalResult[`${url}${r.id}`] = r.fields.map((f) => f.name);
});
resolve(finalResult);
}, (e) => reject(fail(e)));
});
}
//TODO: function doc
export function _updateUrl(url) {
url += url.endsWith("/") ? "" : "/";
return url;
}
//TODO: function doc
export function _getAddFeatures(updateInfos, fields) {
const features = [];
updateInfos.forEach((update) => {
const f = {};
Object.keys(update).forEach((k) => {
const fieldName = _getField(k, fields);
f[fieldName] = update[k];
});
features.push({ attributes: f });
});
return features;
}
//TODO: function doc
export function _getField(name, fields) {
return fields.filter((f) => f.toLowerCase() === name.toLowerCase())[0];
}
/**
* Updates the dispatchers service to include the current user as a dispatcher
*
* @param dispatchers The dispatchers object from the workforce items data
* @param name Current users name
* @param fullName Current users full name
* @param destinationAuthentication The session used to create the new item(s)
* @returns A promise that will resolve with true || false
* @private
*/
export function _updateDispatchers(url, name, fullName, authentication, isPortal) {
return new Promise((resolve, reject) => {
if (url) {
const fieldName = isPortal ? "userid" : "userId";
queryFeatures({
url,
where: `${fieldName} = '${name}'`,
authentication,
}).then((results) => {
if (results && results.features) {
if (results.features.length === 0) {
const features = [
{
attributes: {
name: fullName,
},
},
];
features[0].attributes[fieldName] = name;
_applyEdits(url, features, authentication).then(resolve, reject);
}
else {
resolve(true);
}
}
else {
resolve(false);
}
}, (e) => reject(fail(e)));
}
else {
resolve(false);
}
});
}
//TODO: function doc
export function _applyEdits(url, adds, authentication, useGlobalIds = false) {
return new Promise((resolve, reject) => {
if (adds.length > 0) {
applyEdits({
url,
adds,
useGlobalIds,
authentication,
}).then((addResults) => {
if (addResults && addResults.addResults) {
resolve(true);
}
else {
reject(fail({
success: false,
message: "Failed to add dispatch record.",
}));
}
}, (e) => reject(fail({
success: false,
message: "Failed to add dispatch record.",
error: e,
})));
}
else {
resolve(true);
}
});
}
//#endregion
//# sourceMappingURL=workforceHelpers.js.map