UNPKG

@esri/solution-common

Version:

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

720 lines 31.1 kB
"use strict"; /** @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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports._applyEdits = exports._updateDispatchers = exports._getField = exports._getAddFeatures = exports._updateUrl = exports._getFields = exports.fineTuneCreatedWorkforceItem = exports._getURLs = exports.urlTest = exports.getKeyWorkforceProperties = exports.isWorkforceProject = exports._templatizeWorkforceDispatcherOrWorker = exports._templatizeWorkforceProject = exports.postProcessWorkforceTemplates = exports.getReplaceValue = exports.getLayerId = exports._templatizeUrlTemplate = exports.getUrlDependencies = exports._getAssignmentIntegrationInfos = exports._getAssignmentTypeInfos = exports._updateGlobalIdAndAssignmentType = exports.getWorkforceServiceInfo = exports.getWorkforceDependencies = exports.templatizeWorkforce = exports.extractWorkforceDependencies = exports.convertWorkforceItemToTemplate = void 0; /** * Provides general helper functions. * * @module workforceHelpers */ const arcgisRestJS_1 = require("./arcgisRestJS"); const generalHelpers_1 = require("./generalHelpers"); const templatization_1 = require("./templatization"); const featureServiceHelpers_1 = require("./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 */ 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((0, generalHelpers_1.fail)(e))); } else { resolve(itemTemplate); } }); } exports.convertWorkforceItemToTemplate = convertWorkforceItemToTemplate; /** * 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 */ 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 = (0, generalHelpers_1.getProp)(data, p + ".serviceItemId"); const v = (0, generalHelpers_1.getProp)(data, p); if (serviceItemId) { if (deps.indexOf(serviceItemId) === -1) { deps.push(serviceItemId); } } else { (0, generalHelpers_1.idTest)(v, deps); } }); if ((0, generalHelpers_1.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; (0, generalHelpers_1.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((0, generalHelpers_1.fail)(e))); } else { resolve({ dependencies: deps, urlHash: {}, }); } } else { resolve({ dependencies: deps, urlHash: {}, }); } }); } exports.extractWorkforceDependencies = extractWorkforceDependencies; /** * 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 */ function templatizeWorkforce(data, keyProperties, urlHash, templateDictionary) { keyProperties.forEach((p) => { /* istanbul ignore else */ if ((0, generalHelpers_1.getProp)(data, p)) { if ((0, generalHelpers_1.getProp)(data[p], "serviceItemId")) { // templatize properties with id and url const id = data[p].serviceItemId; let serviceItemIdSuffix = ".itemId"; /* istanbul ignore else */ if ((0, generalHelpers_1.getProp)(data[p], "url")) { const layerId = getLayerId(data[p].url); (0, featureServiceHelpers_1.cacheLayerInfo)(layerId, id, data[p].url, templateDictionary); data[p].url = (0, templatization_1.templatizeTerm)(id, id, getReplaceValue(layerId, ".url")); serviceItemIdSuffix = getReplaceValue(layerId, serviceItemIdSuffix); } data[p].serviceItemId = (0, templatization_1.templatizeTerm)(id, id, serviceItemIdSuffix); } else { // templatize simple id properties data[p] = (0, templatization_1.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; } exports.templatizeWorkforce = templatizeWorkforce; //#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 */ 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 = (0, generalHelpers_1.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: "" }; }); } exports.getWorkforceDependencies = getWorkforceDependencies; function getWorkforceServiceInfo(properties, url, authentication) { return new Promise((resolve, reject) => { url = url.replace("/rest/admin/services", "/rest/services"); url = _updateUrl(url); const requests = [ (0, arcgisRestJS_1.queryFeatures)({ url: `${url}3`, where: "1=1", authentication, }), (0, arcgisRestJS_1.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((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.getWorkforceServiceInfo = getWorkforceServiceInfo; /** * 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 */ 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 = (0, generalHelpers_1.getProp)(workforceInfos, "assignmentIntegrationInfos"); /* istanbul ignore else */ if (assignmentIntegrationInfos && Array.isArray(assignmentIntegrationInfos)) { (0, generalHelpers_1.setProp)(workforceInfos, "assignmentIntegrationInfos", assignmentIntegrationInfos.map(updateId)); } const assignmentTypeInfos = (0, generalHelpers_1.getProp)(workforceInfos, "assignmentTypeInfos"); /* istanbul ignore else */ if (assignmentTypeInfos && Array.isArray(assignmentTypeInfos)) { (0, generalHelpers_1.setProp)(workforceInfos, "assignmentTypeInfos", assignmentTypeInfos.map(updateId)); } } exports._updateGlobalIdAndAssignmentType = _updateGlobalIdAndAssignmentType; //TODO: function doc 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; } exports._getAssignmentTypeInfos = _getAssignmentTypeInfos; //TODO: function doc 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 = (0, generalHelpers_1.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((0, generalHelpers_1.fail)(e))); }); } exports._getAssignmentIntegrationInfos = _getAssignmentIntegrationInfos; 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((0, generalHelpers_1.fail)(e))); } else { resolve({ dependencies, urlHash: {}, }); } }); } exports.getUrlDependencies = getUrlDependencies; /** * 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 */ function _templatizeUrlTemplate(item, urlHash) { // v1 uses urlTemplate // v2 uses urltemplate const urlTemplateVar = (0, generalHelpers_1.getProp)(item, "urlTemplate") ? "urlTemplate" : "urltemplate"; let urlTemplate = (0, generalHelpers_1.getProp)(item, urlTemplateVar); /* istanbul ignore else */ if (urlTemplate) { const ids = (0, generalHelpers_1.getIDs)(urlTemplate); ids.forEach((id) => { urlTemplate = urlTemplate.replace(id, (0, templatization_1.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, (0, templatization_1.templatizeTerm)(id, id, replaceValue)); }); (0, generalHelpers_1.setProp)(item, urlTemplateVar, urlTemplate); } } exports._templatizeUrlTemplate = _templatizeUrlTemplate; function getLayerId(url) { return url.indexOf("FeatureServer/") > -1 ? url.substr(url.lastIndexOf("/") + 1) : undefined; } exports.getLayerId = getLayerId; function getReplaceValue(layerId, suffix) { return isNaN(Number.parseInt(layerId, 10)) ? `${suffix}` : `.layer${layerId}${suffix}`; } exports.getReplaceValue = getReplaceValue; 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; }); } exports.postProcessWorkforceTemplates = postProcessWorkforceTemplates; //TODO: function doc 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] = (0, templatization_1.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 = (0, generalHelpers_1.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; } exports._templatizeWorkforceProject = _templatizeWorkforceProject; //TODO: function doc 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"] = (0, templatization_1.templatizeTerm)(fsId, fsId, ".itemId"); } } return t; } exports._templatizeWorkforceDispatcherOrWorker = _templatizeWorkforceDispatcherOrWorker; // Helpers function isWorkforceProject(itemTemplate) { return (itemTemplate.item.typeKeywords || []).indexOf("Workforce Project") > -1; } exports.isWorkforceProject = isWorkforceProject; function getKeyWorkforceProperties(version) { return version === 1 ? ["groupId", "workerWebMapId", "dispatcherWebMapId", "dispatchers", "assignments", "workers", "tracks"] : ["workforceDispatcherMapId", "workforceProjectGroupId", "workforceWorkerMapId"]; } exports.getKeyWorkforceProperties = getKeyWorkforceProperties; /** * 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 */ function urlTest(v, authentication) { const urls = _getURLs(v); const requests = []; urls.forEach((url) => { const options = { f: "json", authentication: authentication, }; requests.push((0, arcgisRestJS_1.request)(url, options)); }); return { requests: requests, urls: urls, }; } exports.urlTest = urlTest; //TODO: function doc function _getURLs(v) { return (0, generalHelpers_1.regExTest)(v, /=(http.*?FeatureServer.*?(?=&|$))/gi).map((_v) => _v.replace("=", "")); } exports._getURLs = _getURLs; //#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 \} */ 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 = (0, generalHelpers_1.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 = (0, generalHelpers_1.getProp)(newlyCreatedItem, "properties.workforceInfos"); if (workforceInfos && url) { workforceInfos = (0, templatization_1.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((0, generalHelpers_1.fail)(e))); } else { resolve({ success: results }); } }, (e) => reject((0, generalHelpers_1.fail)(e))); }, (e) => reject((0, generalHelpers_1.fail)(e))); }); } exports.fineTuneCreatedWorkforceItem = fineTuneCreatedWorkforceItem; //TODO: function doc 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((0, arcgisRestJS_1.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((0, generalHelpers_1.fail)(e))); }); } exports._getFields = _getFields; //TODO: function doc function _updateUrl(url) { url += url.endsWith("/") ? "" : "/"; return url; } exports._updateUrl = _updateUrl; //TODO: function doc 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; } exports._getAddFeatures = _getAddFeatures; //TODO: function doc function _getField(name, fields) { return fields.filter((f) => f.toLowerCase() === name.toLowerCase())[0]; } exports._getField = _getField; /** * 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 */ function _updateDispatchers(url, name, fullName, authentication, isPortal) { return new Promise((resolve, reject) => { if (url) { const fieldName = isPortal ? "userid" : "userId"; (0, arcgisRestJS_1.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((0, generalHelpers_1.fail)(e))); } else { resolve(false); } }); } exports._updateDispatchers = _updateDispatchers; //TODO: function doc function _applyEdits(url, adds, authentication, useGlobalIds = false) { return new Promise((resolve, reject) => { if (adds.length > 0) { (0, arcgisRestJS_1.applyEdits)({ url, adds, useGlobalIds, authentication, }).then((addResults) => { if (addResults && addResults.addResults) { resolve(true); } else { reject((0, generalHelpers_1.fail)({ success: false, message: "Failed to add dispatch record.", })); } }, (e) => reject((0, generalHelpers_1.fail)({ success: false, message: "Failed to add dispatch record.", error: e, }))); } else { resolve(true); } }); } exports._applyEdits = _applyEdits; //#endregion //# sourceMappingURL=workforceHelpers.js.map