UNPKG

@esri/solution-common

Version:

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

1,042 lines 114 kB
"use strict"; /** @license * Copyright 2019 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._validateEditFieldsInfo = exports._validateTypesTemplates = exports._validateTemplatesFields = exports._validateIndexes = exports._validateDisplayField = exports._validateFields = exports._templatizeLayer = exports._templatizeProperty = exports._templatize = exports.updatePopupInfo = exports._validateViewDomainFields = exports.postProcessFields = exports.updateLayerFieldReferences = exports._getSourceSpatialReferenceAndExtent = exports.validateSpatialReferenceAndExtent = exports.setDefaultSpatialReference = exports._updateTemplateDictionaryFields = exports._updateGeomFieldName = exports._updateSourceLayerFields = exports._updateItemFields = exports._getDynamicFieldNames = exports._getFieldNames = exports._updateForPortal = exports.removeLayerOptimization = exports._isSelfReferential = exports._updateAddOptions = exports._updateOrder = exports.addFeatureServiceDefinition = exports.addFeatureServiceLayersAndTables = exports.getExistingLayersAndTables = exports.getLayersAndTables = exports.deTemplatizeFieldInfos = exports.processContingentValues = exports.updateTemplateForInvalidDesignations = exports.updateSettingsFieldInfos = exports.setNamesAndTitles = exports.getLayerSettings = exports._setTrackingViewLayerSettings = exports._updateTypeKeywords = exports.updateTemplate = exports.cacheLayerInfo = exports._cachePopupInfo = exports.cachePopupInfos = exports._cacheFieldInfo = exports.cacheIndexes = exports.cacheContingentValues = exports.cacheFieldInfos = exports.deleteViewProps = exports.templatize = exports.getFeatureServiceRelatedRecords = void 0; exports._getLayerChunkSize = exports._getNameMapping = exports._templatizeDefinitionQuery = exports._templatizeTimeInfo = exports._templatizeKeys = exports._templatizeTypeTemplates = exports._templatizeTemplates = exports._templatizeLabelingInfo = exports._templatizeArcadeExpressions = exports._templatizeAuthoringInfo = exports._templatizeTemporalRenderer = exports._templatizeGenRenderer = exports._templatizeRenderer = exports._templatizeDrawingInfo = exports._templatizeSimpleName = exports._templatizeDefinitionExpression = exports._templatizeDefinitionEditor = exports._templatizeMediaInfos = exports._templatizePopupElements = exports._templatizeExpressionInfos = exports._templatizeFieldName = exports._templatizePopupInfoFieldInfos = exports._templatizeName = exports._templatizePopupInfo = exports._templatizeRelationshipFields = exports._templatizeTopFilter = exports._templatizeAdminSourceLayerFields = exports._getDependantItemId = exports._templatizeAdminLayerInfoFields = exports._templatizeSourceServiceName = exports._processAdminObject = exports._templatizeAdminLayerInfo = exports._templatizeLayerFieldReferences = void 0; /** * Provides general helper functions. * * @module featureServiceHelpers */ // ------------------------------------------------------------------------------------------------------------------ // //#region Imports -------------------------------------------------------------------------------------------------------// const interfaces_1 = require("./interfaces"); const generalHelpers_1 = require("./generalHelpers"); const templatization_1 = require("./templatization"); const restHelpers_1 = require("./restHelpers"); const trackingHelpers_1 = require("./trackingHelpers"); const arcgisRestJS_1 = require("./arcgisRestJS"); //#endregion ------------------------------------------------------------------------------------------------------------// //#region Public functions ----------------------------------------------------------------------------------------------// /** * Get the related records for a feature service. * * @param url Feature service's URL, e.g., layer.url * @param relationshipId Id of relationship * @param objectIds Objects in the feature service whose related records are sought */ function getFeatureServiceRelatedRecords(url, relationshipId, objectIds) { const options = { url: url + `/${relationshipId}`, relationshipId, objectIds, }; return (0, arcgisRestJS_1.queryRelated)(options); } exports.getFeatureServiceRelatedRecords = getFeatureServiceRelatedRecords; /** * Templatize the ID, url, field references ect * * @param itemTemplate Template for feature service item * @param dependencies Array of IDependency for name mapping * @param templatizeFieldReferences Templatize all field references within a layer * @param templateDictionary Hash mapping property names to replacement values * @returns A promise that will resolve when template has been updated * @private */ function templatize(itemTemplate, dependencies, templatizeFieldReferences, templateDictionary) { templateDictionary = templateDictionary || {}; // Common templatizations const id = itemTemplate.item.id; const fsUrl = itemTemplate.item.url; itemTemplate.item = { ...itemTemplate.item, id: (0, templatization_1.templatizeTerm)(id, id, ".itemId"), url: _templatize(id, "url"), typeKeywords: (0, templatization_1.templatizeIds)(itemTemplate.item.typeKeywords), }; // special handeling if we are dealing with a tracker view (0, trackingHelpers_1.templatizeTracker)(itemTemplate); // added for issue #928 (0, generalHelpers_1.deleteProp)(itemTemplate, "properties.service.size"); const jsonLayers = itemTemplate.properties.layers || []; const jsonTables = itemTemplate.properties.tables || []; const jsonItems = jsonLayers.concat(jsonTables); const data = itemTemplate.data || {}; const layers = data.layers || []; const tables = data.tables || []; const _items = layers.concat(tables); // Set up symbols for the URL of the feature service and its layers and tables templateDictionary[fsUrl] = itemTemplate.item.url; // map FS URL to its templatized form jsonItems.concat(_items).forEach((layer) => { templateDictionary[fsUrl + "/" + layer.id] = _templatize(id, "layer" + layer.id + ".url"); }); // templatize the service references serviceItemId itemTemplate.properties.service.serviceItemId = (0, templatization_1.templatizeTerm)(itemTemplate.properties.service.serviceItemId, itemTemplate.properties.service.serviceItemId, ".itemId"); const initialExtent = (0, generalHelpers_1.getProp)(itemTemplate, "properties.service.initialExtent"); /* istanbul ignore else */ if (initialExtent) { itemTemplate.properties.service.initialExtent = (0, templatization_1.templatizeTerm)(id, id, ".solutionExtent"); } const fullExtent = (0, generalHelpers_1.getProp)(itemTemplate, "properties.service.fullExtent"); /* istanbul ignore else */ if (fullExtent) { itemTemplate.properties.service.fullExtent = (0, templatization_1.templatizeTerm)(id, id, ".solutionExtent"); } // this default extent will be used in cases where it does not make sense to apply the orgs // extent to a service with a local spatial reference itemTemplate.properties.defaultExtent = initialExtent || fullExtent; // in some cases a service does not have a spatial reference defined // added for issue #699 if (!(0, generalHelpers_1.getProp)(itemTemplate, "properties.service.spatialReference") && (0, generalHelpers_1.getProp)(itemTemplate, "properties.defaultExtent.spatialReference")) { (0, generalHelpers_1.setCreateProp)(itemTemplate, "properties.service.spatialReference", itemTemplate.properties.defaultExtent.spatialReference); } // if any layer hasZ enabled then we need to set // enableZDefaults and zDefault to deploy to enterprise let hasZ = false; jsonItems.forEach((jsonItem) => { // get the source service json for the given data item const matchingItems = _items.filter((item) => { return jsonItem.id === item.id; }); // templatize the source service json const _item = matchingItems.length === 1 ? matchingItems[0] : undefined; _templatizeLayer(_item, jsonItem, itemTemplate, dependencies, templatizeFieldReferences, templateDictionary); hasZ = jsonItem.hasZ || (_item && _item.hasZ) ? true : hasZ; }); if (hasZ) { itemTemplate.properties.service.enableZDefaults = true; itemTemplate.properties.service.zDefault = 0; } return itemTemplate; } exports.templatize = templatize; /** * Delete key properties that are system managed * * @param layer The data layer instance with field name references within * @param isPortal When true we are deploying to portal */ function deleteViewProps(layer, isPortal) { const props = ["definitionQuery"]; const portalOnlyProps = ["indexes"]; props.forEach((prop) => (0, generalHelpers_1.deleteProp)(layer, prop)); if (isPortal) { portalOnlyProps.forEach((prop) => { (0, generalHelpers_1.deleteProp)(layer, prop); }); } } exports.deleteViewProps = deleteViewProps; /** * Cache properties that contain field references * * removeProp added for issue #644 * setting all props on add for online now * investigating if we can also just allow them to be set during add for portal * * @param layer The data layer instance with field name references within * @param fieldInfos the object that stores the cached field infos * @param isView When true the current layer is a view and does not need to cache subtype details * @param isPortal When true we are deploying to portal * @returns An updated instance of the fieldInfos */ function cacheFieldInfos(layer, fieldInfos, isView, isPortal) { // cache the source fields as they are in the original source if (layer && layer.fields) { fieldInfos[layer.id] = { sourceFields: JSON.parse(JSON.stringify(layer.fields)), type: layer.type, id: layer.id, }; /* istanbul ignore else */ if (!isView && isPortal) { fieldInfos[layer.id].subtypes = layer.subtypes; fieldInfos[layer.id].subtypeField = layer.subtypeField; fieldInfos[layer.id].defaultSubtypeCode = layer.defaultSubtypeCode; } } // cache each of these properties as they each can contain field references // and will have associated updateDefinition calls when deploying to portal // as well as online for relationships...as relationships added with addToDef will cause failure // https://devtopia.esri.com/WebGIS/solutions-development-support/issues/299 // subtypes, subtypeField, and defaultSubtypeCode should not exist in initial addToDef call and // should be added with subsequent add and update calls in a specific order const props = { editFieldsInfo: false, types: false, templates: false, relationships: true, drawingInfo: false, timeInfo: false, viewDefinitionQuery: false, }; /* istanbul ignore else */ if (!isView && isPortal) { props["subtypes"] = true; props["subtypeField"] = true; props["defaultSubtypeCode"] = true; } Object.keys(props).forEach((k) => { _cacheFieldInfo(layer, k, fieldInfos, props[k]); }); return fieldInfos; } exports.cacheFieldInfos = cacheFieldInfos; /** * Cache the stored contingent values so we can add them in subsequent addToDef calls * * @param id The layer id for the associated values to be stored with * @param fieldInfos The object that stores the cached field infos * @param itemTemplate The current itemTemplate being processed * @returns An updated instance of the fieldInfos */ function cacheContingentValues(id, fieldInfos, itemTemplate) { const contingentValues = (0, generalHelpers_1.getProp)(itemTemplate, "properties.contingentValues"); if (contingentValues && contingentValues[id]) { fieldInfos[id]["contingentValues"] = contingentValues[id]; } return fieldInfos; } exports.cacheContingentValues = cacheContingentValues; /** * Cache the stored contingent values so we can add them in subsequent addToDef calls * * @param layer The current layer to check indexes on * @param fieldInfos The object that stores the cached field infos * @returns An updated instance of the fieldInfos */ function cacheIndexes(layer, fieldInfos, isView, isMsView) { /* istanbul ignore else */ if (!isView && !isMsView && Array.isArray(layer.indexes)) { const oidField = layer.objectIdField; const guidField = layer.globalIdField; fieldInfos[layer.id].indexes = layer.indexes.filter((i) => { if ((i.isUnique && i.fields !== oidField && i.fields !== guidField) || i.indexType === "FullText") { if (i.name) { delete i.name; } return i; } }); delete layer.indexes; } return fieldInfos; } exports.cacheIndexes = cacheIndexes; /** * Helper function to cache a single property into the fieldInfos object * This property will be removed from the layer instance. * * @param layer the data layer being cloned * @param prop the property name used to cache * @param fieldInfos the object that will store the cached property * @param removeProp when true relationships prop will be set to null and subtype props will be deleted * @private */ function _cacheFieldInfo(layer, prop, fieldInfos, removeProp) { /* istanbul ignore else */ if (layer && layer.hasOwnProperty(prop) && fieldInfos && fieldInfos.hasOwnProperty(layer.id)) { fieldInfos[layer.id][prop] = layer[prop]; // editFieldsInfo does not come through unless its with the layer // when it's being added /* istanbul ignore else */ if (removeProp && prop === "relationships") { layer[prop] = null; } else if (removeProp) { delete layer[prop]; } } } exports._cacheFieldInfo = _cacheFieldInfo; /** * Cache popup info that can contain field references * * @param data The items data property * @returns An updated instance of the popupInfos */ function cachePopupInfos(data) { // store any popupInfo so we can update after any potential name changes const popupInfos = { layers: {}, tables: {}, }; if (data && data.layers && data.layers.length > 0) { _cachePopupInfo(popupInfos, "layers", data.layers); } if (data && data.tables && data.tables.length > 0) { _cachePopupInfo(popupInfos, "tables", data.tables); } return popupInfos; } exports.cachePopupInfos = cachePopupInfos; /** * Helper function to cache a single popupInfo * This property will be reset on the layer * * @param popupInfos object to store the cahced popupInfo * @param type is it a layer or table * @param _items list or either layers or tables * @private */ function _cachePopupInfo(popupInfos, type, _items) { _items.forEach((item) => { if (item && item.hasOwnProperty("popupInfo")) { popupInfos[type][item.id] = item.popupInfo; item.popupInfo = {}; } }); } exports._cachePopupInfo = _cachePopupInfo; /** * Store basic layer information for potential replacement if we are unable to access a given service * added for issue #859 * * @param layerId the id for the layer * @param itemId the id for the item * @param url the url for the layer * @param templateDictionary Hash of key details used for variable replacement * @returns templatized itemTemplate */ function cacheLayerInfo(layerId, itemId, url, templateDictionary) { if (layerId) { const layerIdVar = `layer${layerId}`; // need to structure these differently so they are not used for standard replacement calls // this now adds additional vars that are not needing replacement unless we fail to fetch the service const newVars = (0, generalHelpers_1.getProp)(templateDictionary, `${interfaces_1.UNREACHABLE}.${itemId}`) || { itemId, }; newVars[layerIdVar] = (0, generalHelpers_1.getProp)(newVars, layerIdVar) || { layerId, itemId, }; if (url !== "") { newVars[layerIdVar]["url"] = url; } const unreachableVars = {}; unreachableVars[itemId] = newVars; templateDictionary[interfaces_1.UNREACHABLE] = { ...templateDictionary[interfaces_1.UNREACHABLE], ...unreachableVars, }; } } exports.cacheLayerInfo = cacheLayerInfo; /** * Creates an item in a specified folder (except for Group item type). * * @param itemTemplate Item to be created; n.b.: this item is modified * @param templateDictionary Hash mapping property names to replacement values * @param createResponse Response from create service * @returns An updated instance of the template * @private */ function updateTemplate(itemTemplate, templateDictionary, createResponse) { // Update the item with any typeKeywords that were added on create _updateTypeKeywords(itemTemplate, createResponse); // Add the new item to the template dictionary templateDictionary[itemTemplate.itemId] = Object.assign(templateDictionary[itemTemplate.itemId] || {}, { itemId: createResponse.serviceItemId, url: (0, generalHelpers_1.checkUrlPathTermination)(createResponse.serviceurl), name: createResponse.name, }); // Update the item template now that the new service has been created itemTemplate.itemId = createResponse.serviceItemId; return (0, templatization_1.replaceInTemplate)(itemTemplate, templateDictionary); } exports.updateTemplate = updateTemplate; /** * Updates the items typeKeywords to include any typeKeywords that * were added by the create service request * * @param itemTemplate Item to be created; n.b.: this item is modified * @param createResponse Response from create service * @returns An updated instance of the template * @private */ function _updateTypeKeywords(itemTemplate, createResponse) { // https://github.com/Esri/solution.js/issues/589 const iKwords = (0, generalHelpers_1.getProp)(itemTemplate, "item.typeKeywords"); const cKwords = (0, generalHelpers_1.getProp)(createResponse, "typeKeywords"); if (iKwords && cKwords) { (0, generalHelpers_1.setProp)(itemTemplate, "item.typeKeywords", iKwords.concat(cKwords.filter((k) => iKwords.indexOf(k) < 0))); } return itemTemplate; } exports._updateTypeKeywords = _updateTypeKeywords; /** * Add layer urls from tracking views to the templateDictionary to be used for adlib replacements * * @param itemTemplate Item to be created; n.b.: this item is modified * @param templateDictionary Hash mapping property names to replacement values * @returns void * @private */ function _setTrackingViewLayerSettings(itemTemplate, templateDictionary) { const url = itemTemplate.item.url; const newId = itemTemplate.itemId; let k; Object.keys(templateDictionary).some((_k) => { if (newId === templateDictionary[_k].itemId) { k = _k; return true; } }); itemTemplate.properties.layers.forEach((l) => { const id = l.id.toString(); templateDictionary[k][`layer${id}`] = { url: (0, generalHelpers_1.checkUrlPathTermination)(url) + id, }; }); } exports._setTrackingViewLayerSettings = _setTrackingViewLayerSettings; /** * Create the name mapping object that will allow for all templatized field * references to be de-templatized. * This also removes the stored sourceFields and newFields arrays from fieldInfos. * * @example * \{ layer0: \{ fields: \{ lowerCaseSourceFieldName: newFieldNameAfterDeployment \} \} \} * * @param layerInfos The object that stores the cached layer properties and name mapping * @returns The settings object that will be used to de-templatize the field references. */ function getLayerSettings(layerInfos, url, itemId, enterpriseIDMapping) { const settings = {}; const ids = Object.keys(layerInfos); ids.forEach((id) => { const _layerId = (0, generalHelpers_1.getProp)(layerInfos[id], "item.id"); const isNum = parseInt(_layerId, 10) > -1; const layerId = isNum && enterpriseIDMapping ? enterpriseIDMapping[_layerId] : isNum ? _layerId : id; settings[`layer${isNum ? _layerId : id}`] = { fields: _getNameMapping(layerInfos, id), url: (0, generalHelpers_1.checkUrlPathTermination)(url) + layerId, layerId, itemId, }; (0, generalHelpers_1.deleteProp)(layerInfos[id], "newFields"); (0, generalHelpers_1.deleteProp)(layerInfos[id], "sourceFields"); }); return settings; } exports.getLayerSettings = getLayerSettings; /** * Set the names and titles for all feature services. * * This function will ensure that we have unique feature service names. * The feature service name will have a generated GUID appended. * * @param templates A collection of AGO item templates. * @returns An updated collection of AGO templates with unique feature service names. */ function setNamesAndTitles(templates) { const guid = (0, generalHelpers_1.generateGUID)(); const names = []; return templates.map((t) => { /* istanbul ignore else */ if (t.item.type === "Feature Service") { // Retain the existing title but swap with name if it's missing t.item.title = t.item.title || t.item.name; /* istanbul ignore else */ if (!(0, trackingHelpers_1.isTrackingViewTemplate)(t)) { // Need to set the service name: name + "_" + newItemId let baseName = t.item.name || t.item.title; // If the name already contains a GUID remove it baseName = baseName.replace(/_[0-9A-F]{32}/gi, ""); // The name length limit is 98 // Limit the baseName to 50 characters before the _<guid> const name = baseName.substring(0, 50) + "_" + guid; // If the name + GUID already exists then append "_occurrenceCount" t.item.name = names.indexOf(name) === -1 ? name : `${name}_${names.filter((n) => n === name).length}`; names.push(name); } } return t; }); } exports.setNamesAndTitles = setNamesAndTitles; /** * This is used when deploying views. * We need to update fields referenced in adminLayerInfo for relationships prior to deploying the view. * This moves the fieldInfos for the views source layers from the item settings for the source layer * to the item settings for the view. * * @param itemTemplate The current itemTemplate being processed. * @param settings The settings object used to de-templatize the various templates within the item. */ function updateSettingsFieldInfos(itemTemplate, settings) { const dependencies = itemTemplate.dependencies; const id = itemTemplate.itemId; const settingsKeys = Object.keys(settings); settingsKeys.forEach((k) => { if (id === settings[k].itemId) { dependencies.forEach((d) => { settingsKeys.forEach((_k) => { /* istanbul ignore else */ if (d === _k) { // combine for multi-source views const fieldInfos = {}; fieldInfos[d] = (0, generalHelpers_1.getProp)(settings[_k], "fieldInfos"); settings[k]["sourceServiceFields"] = settings[k]["sourceServiceFields"] ? { ...settings[k]["sourceServiceFields"], ...fieldInfos } : fieldInfos; const layerKeys = Object.keys(settings[_k]); layerKeys.forEach((layerKey) => { /* istanbul ignore else */ if (layerKey.startsWith("layer")) { settings[k][layerKey] = settings[_k][layerKey]; } }); } }); }); } }); } exports.updateSettingsFieldInfos = updateSettingsFieldInfos; /** * Add flag to indicate item should be ignored. * Construct template dictionary to detemplatize any references to this item by other items. * * @param template Template for feature service item * @param authentication Credentials for the request * @returns A promise that will resolve when template has been updated * @private */ function updateTemplateForInvalidDesignations(template, authentication) { return new Promise((resolve, reject) => { template.properties.hasInvalidDesignations = true; if (template.item.url) { // get the admin URL const url = template.item.url; (0, arcgisRestJS_1.request)(url + "?f=json", { authentication: authentication, }).then((serviceData) => { const layerInfos = {}; const layersAndTables = (serviceData.layers || []).concat(serviceData.tables || []); layersAndTables.forEach((l) => { /* istanbul ignore else */ if (l && l.hasOwnProperty("id")) { layerInfos[l.id] = l; } }); template.data[template.itemId] = Object.assign({ itemId: template.itemId, }, getLayerSettings(layerInfos, url, template.itemId)); resolve(template); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else { resolve(template); } }); } exports.updateTemplateForInvalidDesignations = updateTemplateForInvalidDesignations; /** * Get the contingent values for each layer in the service. * Remove key props that cannot be included with the addToDef call on deploy. * Store the values alongside other key feature service properties in the template * * @param properties the current feature services properties * @param adminUrl the current feature service url * @param authentication Credentials for the request to AGOL * @returns A promise that will resolve when the contingent values have been fetched. * This function will update the provided properties argument when contingent values are found. */ function processContingentValues(properties, adminUrl, authentication) { return new Promise((resolve, reject) => { if ((0, generalHelpers_1.getProp)(properties, "service.isView")) { // views will inherit from the source service resolve(); } else { const layersAndTables = (properties.layers || []).concat(properties.tables || []); const layerIds = []; const contingentValuePromises = layersAndTables.reduce((prev, cur) => { /* istanbul ignore else */ if (cur.hasContingentValuesDefinition) { prev.push((0, arcgisRestJS_1.request)(`${adminUrl}/${cur["id"]}/contingentValues?f=json`, { authentication, })); layerIds.push(cur["id"]); } return prev; }, []); if (contingentValuePromises.length > 0) { Promise.all(contingentValuePromises).then((results) => { const contingentValues = {}; results.forEach((r, i) => { (0, generalHelpers_1.deleteProp)(r, "typeCodes"); /* istanbul ignore else */ if ((0, generalHelpers_1.getProp)(r, "stringDicts") && (0, generalHelpers_1.getProp)(r, "contingentValuesDefinition")) { r.contingentValuesDefinition["stringDicts"] = r.stringDicts; (0, generalHelpers_1.deleteProp)(r, "stringDicts"); } (0, generalHelpers_1.deleteProps)((0, generalHelpers_1.getProp)(r, "contingentValuesDefinition"), [ "layerID", "layerName", "geometryType", "hasSubType", ]); contingentValues[layerIds[i]] = r; }); properties.contingentValues = contingentValues; resolve(); }, reject); } else { resolve(); } } }); } exports.processContingentValues = processContingentValues; /** * Replace the field name reference templates with the new field names after deployment. * * @param fieldInfos The object that stores the cached layer properties and name mapping * @param popupInfos The object from the popupInfo property for the layer * @param adminLayerInfos The object from the adminLayerInfo property for the layer * @param settings The settings object that has all of the mappings for de-templatizing. * @returns An object that contains updated instances of popupInfos, fieldInfos, and adminLayerInfos */ function deTemplatizeFieldInfos(fieldInfos, popupInfos, adminLayerInfos, settings) { const fieldInfoKeys = Object.keys(fieldInfos); fieldInfoKeys.forEach((id) => { if (fieldInfos[id].hasOwnProperty("templates")) { fieldInfos[id].templates = JSON.parse((0, templatization_1.replaceInTemplate)(JSON.stringify(fieldInfos[id].templates), settings)); } if (fieldInfos[id].hasOwnProperty("adminLayerInfo")) { adminLayerInfos[id].viewLayerDefinition.table.relatedTables = fieldInfos[id].adminLayerInfo; (0, generalHelpers_1.deleteProp)(fieldInfos[id], "adminLayerInfo"); } if (fieldInfos[id].hasOwnProperty("types")) { fieldInfos[id].types = JSON.parse((0, templatization_1.replaceInTemplate)(JSON.stringify(fieldInfos[id].types), settings)); } }); return { popupInfos: (0, templatization_1.replaceInTemplate)(popupInfos, settings), fieldInfos: (0, templatization_1.replaceInTemplate)(fieldInfos, settings), adminLayerInfos: (0, templatization_1.replaceInTemplate)(adminLayerInfos, settings), }; } exports.deTemplatizeFieldInfos = deTemplatizeFieldInfos; /** * This is used when deploying views. * We need to update fields referenced in adminLayerInfo for relationships prior to deploying the view. * This moves the fieldInfos for the views source layers from the item settings for the source layer * to the item settings for the view. * * @param itemTemplate The current itemTemplate being processed. * @returns array of layers and tables */ function getLayersAndTables(itemTemplate) { const properties = itemTemplate.properties; const layersAndTables = []; (properties.layers || []).forEach(function (layer) { layersAndTables.push({ item: layer, type: "layer", }); }); (properties.tables || []).forEach(function (table) { layersAndTables.push({ item: table, type: "table", }); }); return layersAndTables; } exports.getLayersAndTables = getLayersAndTables; /** * Fetch each layer and table from service so we can determine what fields they have. * This is leveraged when we are using existing services so we can determine if we need to * remove any fields from views that depend on these layers and tables. * * @param url Feature service endpoint * @param ids layer and table ids * @param authentication Credentials for the request * @returns A promise that will resolve an array of promises with either a failure or the data * @private */ function getExistingLayersAndTables(url, ids, authentication) { // eslint-disable-next-line @typescript-eslint/no-floating-promises return new Promise((resolve) => { const defs = ids.map((id) => { return (0, arcgisRestJS_1.request)((0, generalHelpers_1.checkUrlPathTermination)(url) + id, { authentication, }); }); // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all(defs.map((p) => p.catch((e) => e))).then(resolve); }); } exports.getExistingLayersAndTables = getExistingLayersAndTables; /** * Adds the layers and tables of a feature service to it and restores their relationships. * * @param itemTemplate Feature service * @param templateDictionary Hash mapping Solution source id to id of its clone (and name & URL for feature * service) * @param popupInfos the cached popup info from the layers * @param authentication Credentials for the request * @returns A promise that will resolve when all layers and tables have been added * @private */ function addFeatureServiceLayersAndTables(itemTemplate, templateDictionary, popupInfos, authentication) { return new Promise((resolve, reject) => { if ((0, trackingHelpers_1.isTrackingViewTemplate)(itemTemplate)) { _setTrackingViewLayerSettings(itemTemplate, templateDictionary); resolve(null); } else { // Create a hash of various properties that contain field references const fieldInfos = {}; const adminLayerInfos = {}; // Add the service's layers and tables to it const layersAndTables = getLayersAndTables(itemTemplate); if (layersAndTables.length > 0) { addFeatureServiceDefinition(itemTemplate.item.url || "", layersAndTables, templateDictionary, authentication, itemTemplate.key, adminLayerInfos, fieldInfos, itemTemplate).then(() => { // Detemplatize field references and update the layer properties // Only failure path is handled by addFeatureServiceDefinition // eslint-disable-next-line @typescript-eslint/no-floating-promises updateLayerFieldReferences(itemTemplate, fieldInfos, popupInfos, adminLayerInfos, templateDictionary).then((r) => { // Update relationships and layer definitions const updates = (0, restHelpers_1.getLayerUpdates)({ message: "updated layer definition", objects: r.layerInfos.fieldInfos, itemTemplate: r.itemTemplate, authentication, }, templateDictionary.isPortal); // Process the updates sequentially updates .reduce((prev, update) => { return prev.then(() => { return (0, restHelpers_1.getRequest)(update, false, false, templateDictionary.isPortal); }); }, Promise.resolve(null)) .then(() => resolve(null), (e) => reject((0, generalHelpers_1.fail)(e))); }); }, (e) => reject((0, generalHelpers_1.fail)(e))); } else { resolve(null); } } }); } exports.addFeatureServiceLayersAndTables = addFeatureServiceLayersAndTables; /** * Updates a feature service with a list of layers and/or tables. * * @param serviceUrl URL of feature service * @param listToAdd List of layers and/or tables to add * @param templateDictionary Hash mapping Solution source id to id of its clone (and name & URL for feature * service) * @param authentication Credentials for the request * @param key * @param adminLayerInfos Hash map of a layers adminLayerInfo * @param fieldInfos Hash map of properties that contain field references * @param itemTemplate * @returns A promise that will resolve when the feature service has been updated * @private */ function addFeatureServiceDefinition(serviceUrl, listToAdd, templateDictionary, authentication, key, adminLayerInfos, fieldInfos, itemTemplate) { return new Promise((resolve, reject) => { if ((0, trackingHelpers_1.isTrackingViewTemplate)(itemTemplate)) { resolve(null); } else { let options = { layers: [], tables: [], authentication, }; // if the service has veiws keep track of the fields so we can use them to // compare with the view fields /* istanbul ignore else */ if ((0, generalHelpers_1.getProp)(itemTemplate, "properties.service.hasViews")) { _updateTemplateDictionaryFields(itemTemplate, templateDictionary); } const isSelfReferential = _isSelfReferential(listToAdd); listToAdd = _updateOrder(listToAdd, isSelfReferential, itemTemplate); const chunkSize = _getLayerChunkSize(); const layerChunks = []; listToAdd.forEach((toAdd, i) => { let item = toAdd.item; const originalId = item.id; const isView = itemTemplate.properties.service.isView; const isMsView = itemTemplate.properties.service.isMultiServicesView; const isPortal = templateDictionary.isPortal; fieldInfos = cacheFieldInfos(item, fieldInfos, isView, isPortal); // cache the values to be added in seperate addToDef calls fieldInfos = cacheContingentValues(item.id, fieldInfos, itemTemplate); // cache specific field indexes when deploying to ArcGIS Enterprise portal if (isPortal) { fieldInfos = cacheIndexes(item, fieldInfos, isView, isMsView); } /* istanbul ignore else */ if (item.isView) { deleteViewProps(item, isPortal); } // when the item is a view we need to grab the supporting fieldInfos /* istanbul ignore else */ if (isView) { _updateGeomFieldName(item.adminLayerInfo, templateDictionary); adminLayerInfos[originalId] = item.adminLayerInfo; // need to update adminLayerInfo before adding to the service def // bring over the fieldInfos from the source layer updateSettingsFieldInfos(itemTemplate, templateDictionary); // update adminLayerInfo before add to definition with view source fieldInfo settings item.adminLayerInfo = (0, templatization_1.replaceInTemplate)(item.adminLayerInfo, templateDictionary); /* istanbul ignore else */ if (fieldInfos && fieldInfos.hasOwnProperty(item.id)) { Object.keys(templateDictionary).some((k) => { if (templateDictionary[k].itemId === itemTemplate.itemId) { fieldInfos[item.id]["sourceServiceFields"] = templateDictionary[k].sourceServiceFields; return true; } else { return false; } }); _validateViewDomainFields(item, isPortal, isMsView); } } /* istanbul ignore else */ if (isPortal) { item = _updateForPortal(item, itemTemplate, templateDictionary); } removeLayerOptimization(item); // this can still chunk layers options = _updateAddOptions(itemTemplate, options, layerChunks, isSelfReferential, authentication); if (item.type === "Feature Layer") { options.layers.push(item); } else { options.tables.push(item); } // In general we are switching to not use chunking. Rather if we exceed the defined chunk size // we will use an async request. // Currently the only case that should chunk the requests is when we have a multisource view // handled in _updateAddOptions above /* istanbul ignore else */ if (i + 1 === listToAdd.length) { layerChunks.push(Object.assign({}, options)); options = { layers: [], tables: [], authentication, }; } }); // will use async by default rather than chunk the layer requests when we have more layers // than the defined chunk size const useAsync = listToAdd.length > chunkSize; layerChunks .reduce((prev, curr) => prev.then(() => (0, restHelpers_1.addToServiceDefinition)(serviceUrl, curr, false, useAsync)), Promise.resolve(null)) .then(() => resolve(null), (e) => reject((0, generalHelpers_1.fail)(e))); } }); } exports.addFeatureServiceDefinition = addFeatureServiceDefinition; /** * When a view is a multi service view sort based on the id * https://github.com/Esri/solution.js/issues/1048 * * @param layersAndTables The list of layers and tables for the current template * @param isSelfReferential Indicates if any layers or tables have relationships with other layers or tables in the same service * @param itemTemplate The current itemTemplate being processed * * @returns Sorted list of layers and tables when using a multi-service view * @private */ function _updateOrder(layersAndTables, isSelfReferential, itemTemplate) { const isMsView = (0, generalHelpers_1.getProp)(itemTemplate, "properties.service.isMultiServicesView") || false; return isSelfReferential || isMsView ? layersAndTables.sort((a, b) => a.item.id - b.item.id) : layersAndTables; } exports._updateOrder = _updateOrder; /** * When a view is a multi service view add each layer separately * https://github.com/Esri/solution.js/issues/871 * * @param itemTemplate The current itemTemplate being processed * @param options Add to service definition options * @param layerChunks Groups of layers or tables to add to the service * @param isSelfReferential Indicates if any layers or tables have relationships with other layers or tables in the same service * @param authentication Credentials for the request * * @returns Add to service definition options * @private */ function _updateAddOptions(itemTemplate, options, layerChunks, isSelfReferential, authentication) { const isMsView = (0, generalHelpers_1.getProp)(itemTemplate, "properties.service.isMultiServicesView") || false; /* istanbul ignore else */ if (isMsView || isSelfReferential) { // if we already have some layers or tables add them first /* istanbul ignore else */ if (options.layers.length > 0 || options.tables.length > 0) { layerChunks.push(Object.assign({}, options)); options = { layers: [], tables: [], authentication, }; } } return options; } exports._updateAddOptions = _updateAddOptions; /** * Determine if any layer or table within the service references * other layers or tables within the same service * * @param layersAndTables the list of layers and tables from the service * * @returns true when valid internal references are found * @private */ function _isSelfReferential(layersAndTables) { const names = layersAndTables.map((l) => l.item.name); const srcTables = {}; return layersAndTables.some((l) => { const table = l.item.adminLayerInfo?.viewLayerDefinition?.table; if (table) { const name = table.sourceServiceName; const id = table.sourceLayerId; if (name && id > -1) { if (Object.keys(srcTables).indexOf(name) > -1) { if (srcTables[name].indexOf(id) > -1) { return true; } else { srcTables[name].push(id); } } else { srcTables[name] = [id]; } } return (table.relatedTables || []).some((r) => names.indexOf(r.name) > -1); } }); } exports._isSelfReferential = _isSelfReferential; /** * Remove "multiScaleGeometryInfo" for issue #526 to prevent invalid enablement of layer optimization * * @param layer the layer to evaluate * @private */ function removeLayerOptimization(layer) { /* istanbul ignore else */ if (layer.multiScaleGeometryInfo) { (0, generalHelpers_1.deleteProp)(layer, "multiScaleGeometryInfo"); } } exports.removeLayerOptimization = removeLayerOptimization; /** * Handle portal specific updates to the item * * @param item the item to update * @param itemTemplate the item template * @param templateDictionary Hash mapping Solution source id to id of its clone * * @returns the updated item * @private */ function _updateForPortal(item, itemTemplate, templateDictionary) { // When deploying to portal we need to adjust the uniquie ID field up front /* istanbul ignore else */ if (item.uniqueIdField && item.uniqueIdField.name) { item.uniqueIdField.name = String(item.uniqueIdField.name).toLocaleLowerCase(); } // Portal will fail if the geometryField is null if (item.type === "Table" && item.adminLayerInfo) { (0, generalHelpers_1.deleteProp)(item.adminLayerInfo, "geometryField"); } // Portal will fail if the sourceFields in the viewLayerDef contain fields that are not in the source service /* istanbul ignore else */ if (item.isView) { const viewLayerDefTable = (0, generalHelpers_1.getProp)(item, "adminLayerInfo.viewLayerDefinition.table"); let fieldNames = []; if (viewLayerDefTable) { const tableFieldNames = _getFieldNames(viewLayerDefTable, itemTemplate, templateDictionary); fieldNames = fieldNames.concat(tableFieldNames); const dynamicFieldNames = _getDynamicFieldNames(viewLayerDefTable); fieldNames = fieldNames.concat(dynamicFieldNames); (0, generalHelpers_1.setProp)(item, "adminLayerInfo.viewLayerDefinition.table", _updateSourceLayerFields(viewLayerDefTable, fieldNames)); // Handle related also /* istanbul ignore else */ if (Array.isArray(viewLayerDefTable.relatedTables)) { viewLayerDefTable.relatedTables.map((relatedTable) => { const relatedTableFieldNames = _getFieldNames(relatedTable, itemTemplate, templateDictionary); fieldNames = fieldNames.concat(relatedTableFieldNames); const dynamicRelatedFieldNames = _getDynamicFieldNames(relatedTable); fieldNames = fieldNames.concat(dynamicRelatedFieldNames); return _updateSourceLayerFields(relatedTable, [...relatedTableFieldNames, ...dynamicRelatedFieldNames]); }); } } else { Object.keys(templateDictionary).some((k) => { /* istanbul ignore else */ if (templateDictionary[k].itemId === item.serviceItemId) { const layerInfo = templateDictionary[k][`layer${item.id}`]; /* istanbul ignore else */ if (layerInfo && layerInfo.fields) { if (Array.isArray(layerInfo.fields)) { fieldNames = layerInfo.fields.map((f) => f.name); } else { fieldNames = Object.keys(layerInfo.fields); } } return true; } }); } item = _updateItemFields(item, fieldNames); } // not allowed to set sourceSchemaChangesAllowed or isView for portal // these are set when you create the service (0, generalHelpers_1.deleteProp)(item, "isView"); return item; } exports._updateForPortal = _updateForPortal; /** * Get a list of the source layer field names * * @param table the table instance to compare * @param itemTemplate the item template * @param templateDictionary Hash mapping Solution source id to id of its clone * * @returns an array of the source layers fields * @private */ function _getFieldNames(table, itemTemplate, templateDictionary) { let sourceLayerFields = []; const viewSourceLayerId = table.sourceLayerId; /* istanbul ignore else */ if (typeof viewSourceLayerId === "number") { // need to make sure these actually exist in the source.. itemTemplate.dependencies.forEach((d) => { const layerInfo = templateDictionary[d][`layer${viewSourceLayerId}`]; /* istanbul ignore else */ if (layerInfo && layerInfo.fie