UNPKG

@esri/solution-common

Version:

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

691 lines 28 kB
/** @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