UNPKG

@esri/solution-common

Version:

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

1,147 lines 104 kB
/** @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. */ /** * Provides general helper functions. * * @module featureServiceHelpers */ // ------------------------------------------------------------------------------------------------------------------ // //#region Imports -------------------------------------------------------------------------------------------------------// import { UNREACHABLE, } from "./interfaces"; import { checkUrlPathTermination, deleteProp, deleteProps, fail, generateGUID, getProp, setCreateProp, setProp, } from "./generalHelpers"; import { replaceInTemplate, templatizeTerm, templatizeIds } from "./templatization"; import { addToServiceDefinition, getLayerUpdates, getRequest } from "./restHelpers"; import { isTrackingViewTemplate, templatizeTracker } from "./trackingHelpers"; import { queryRelated, request, } from "./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 */ export function getFeatureServiceRelatedRecords(url, relationshipId, objectIds) { const options = { url: url + `/${relationshipId}`, relationshipId, objectIds, }; return queryRelated(options); } /** * 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 */ export 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: templatizeTerm(id, id, ".itemId"), url: _templatize(id, "url"), typeKeywords: templatizeIds(itemTemplate.item.typeKeywords), }; // special handeling if we are dealing with a tracker view templatizeTracker(itemTemplate); // added for issue #928 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 = templatizeTerm(itemTemplate.properties.service.serviceItemId, itemTemplate.properties.service.serviceItemId, ".itemId"); const initialExtent = getProp(itemTemplate, "properties.service.initialExtent"); /* istanbul ignore else */ if (initialExtent) { itemTemplate.properties.service.initialExtent = templatizeTerm(id, id, ".solutionExtent"); } const fullExtent = getProp(itemTemplate, "properties.service.fullExtent"); /* istanbul ignore else */ if (fullExtent) { itemTemplate.properties.service.fullExtent = 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 (!getProp(itemTemplate, "properties.service.spatialReference") && getProp(itemTemplate, "properties.defaultExtent.spatialReference")) { 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; } /** * 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 */ export function deleteViewProps(layer, isPortal) { const props = ["definitionQuery"]; const portalOnlyProps = ["indexes"]; props.forEach((prop) => deleteProp(layer, prop)); if (isPortal) { portalOnlyProps.forEach((prop) => { deleteProp(layer, prop); }); } } /** * 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 */ export 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; } /** * 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 */ export function cacheContingentValues(id, fieldInfos, itemTemplate) { const contingentValues = getProp(itemTemplate, "properties.contingentValues"); if (contingentValues && contingentValues[id]) { fieldInfos[id]["contingentValues"] = contingentValues[id]; } return fieldInfos; } /** * 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 */ export 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; } /** * 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 */ export 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]; } } } /** * Cache popup info that can contain field references * * @param data The items data property * @returns An updated instance of the popupInfos */ export 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; } /** * 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 */ export function _cachePopupInfo(popupInfos, type, _items) { _items.forEach((item) => { if (item && item.hasOwnProperty("popupInfo")) { popupInfos[type][item.id] = item.popupInfo; item.popupInfo = {}; } }); } /** * 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 */ export 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 = getProp(templateDictionary, `${UNREACHABLE}.${itemId}`) || { itemId, }; newVars[layerIdVar] = getProp(newVars, layerIdVar) || { layerId, itemId, }; if (url !== "") { newVars[layerIdVar]["url"] = url; } const unreachableVars = {}; unreachableVars[itemId] = newVars; templateDictionary[UNREACHABLE] = { ...templateDictionary[UNREACHABLE], ...unreachableVars, }; } } /** * 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 */ export 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: checkUrlPathTermination(createResponse.serviceurl), name: createResponse.name, }); // Update the item template now that the new service has been created itemTemplate.itemId = createResponse.serviceItemId; return replaceInTemplate(itemTemplate, templateDictionary); } /** * 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 */ export function _updateTypeKeywords(itemTemplate, createResponse) { // https://github.com/Esri/solution.js/issues/589 const iKwords = getProp(itemTemplate, "item.typeKeywords"); const cKwords = getProp(createResponse, "typeKeywords"); if (iKwords && cKwords) { setProp(itemTemplate, "item.typeKeywords", iKwords.concat(cKwords.filter((k) => iKwords.indexOf(k) < 0))); } return itemTemplate; } /** * 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 */ export 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: checkUrlPathTermination(url) + id, }; }); } /** * 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. */ export function getLayerSettings(layerInfos, url, itemId, enterpriseIDMapping) { const settings = {}; const ids = Object.keys(layerInfos); ids.forEach((id) => { const _layerId = 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: checkUrlPathTermination(url) + layerId, layerId, itemId, }; deleteProp(layerInfos[id], "newFields"); deleteProp(layerInfos[id], "sourceFields"); }); return settings; } /** * 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. */ export function setNamesAndTitles(templates) { const guid = 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 (!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; }); } /** * 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. */ export 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] = 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]; } }); } }); }); } }); } /** * 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 */ export 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; 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(fail(e))); } else { resolve(template); } }); } /** * 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. */ export function processContingentValues(properties, adminUrl, authentication) { return new Promise((resolve, reject) => { if (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(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) => { deleteProp(r, "typeCodes"); /* istanbul ignore else */ if (getProp(r, "stringDicts") && getProp(r, "contingentValuesDefinition")) { r.contingentValuesDefinition["stringDicts"] = r.stringDicts; deleteProp(r, "stringDicts"); } deleteProps(getProp(r, "contingentValuesDefinition"), [ "layerID", "layerName", "geometryType", "hasSubType", ]); contingentValues[layerIds[i]] = r; }); properties.contingentValues = contingentValues; resolve(); }, reject); } else { resolve(); } } }); } /** * 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 */ export function deTemplatizeFieldInfos(fieldInfos, popupInfos, adminLayerInfos, settings) { const fieldInfoKeys = Object.keys(fieldInfos); fieldInfoKeys.forEach((id) => { if (fieldInfos[id].hasOwnProperty("templates")) { fieldInfos[id].templates = JSON.parse(replaceInTemplate(JSON.stringify(fieldInfos[id].templates), settings)); } if (fieldInfos[id].hasOwnProperty("adminLayerInfo")) { adminLayerInfos[id].viewLayerDefinition.table.relatedTables = fieldInfos[id].adminLayerInfo; deleteProp(fieldInfos[id], "adminLayerInfo"); } if (fieldInfos[id].hasOwnProperty("types")) { fieldInfos[id].types = JSON.parse(replaceInTemplate(JSON.stringify(fieldInfos[id].types), settings)); } }); return { popupInfos: replaceInTemplate(popupInfos, settings), fieldInfos: replaceInTemplate(fieldInfos, settings), adminLayerInfos: replaceInTemplate(adminLayerInfos, settings), }; } /** * 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 */ export 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; } /** * 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 */ export function getExistingLayersAndTables(url, ids, authentication) { // eslint-disable-next-line @typescript-eslint/no-floating-promises return new Promise((resolve) => { const defs = ids.map((id) => { return request(checkUrlPathTermination(url) + id, { authentication, }); }); // eslint-disable-next-line @typescript-eslint/no-floating-promises Promise.all(defs.map((p) => p.catch((e) => e))).then(resolve); }); } /** * 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 */ export function addFeatureServiceLayersAndTables(itemTemplate, templateDictionary, popupInfos, authentication) { return new Promise((resolve, reject) => { if (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 = 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 getRequest(update, false, false, templateDictionary.isPortal); }); }, Promise.resolve(null)) .then(() => resolve(null), (e) => reject(fail(e))); }); }, (e) => reject(fail(e))); } else { resolve(null); } } }); } /** * 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 */ export function addFeatureServiceDefinition(serviceUrl, listToAdd, templateDictionary, authentication, key, adminLayerInfos, fieldInfos, itemTemplate) { return new Promise((resolve, reject) => { if (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 (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 = 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(() => addToServiceDefinition(serviceUrl, curr, false, useAsync)), Promise.resolve(null)) .then(() => resolve(null), (e) => reject(fail(e))); } }); } /** * 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 */ export function _updateOrder(layersAndTables, isSelfReferential, itemTemplate) { const isMsView = getProp(itemTemplate, "properties.service.isMultiServicesView") || false; return isSelfReferential || isMsView ? layersAndTables.sort((a, b) => a.item.id - b.item.id) : layersAndTables; } /** * 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 */ export function _updateAddOptions(itemTemplate, options, layerChunks, isSelfReferential, authentication) { const isMsView = 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; } /** * 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 */ export 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); } }); } /** * Remove "multiScaleGeometryInfo" for issue #526 to prevent invalid enablement of layer optimization * * @param layer the layer to evaluate * @private */ export function removeLayerOptimization(layer) { /* istanbul ignore else */ if (layer.multiScaleGeometryInfo) { deleteProp(layer, "multiScaleGeometryInfo"); } } /** * 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 */ export 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) { 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 = 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); 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 deleteProp(item, "isView"); return item; } /** * 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 */ export 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.fields && templateDictionary[d].name === table.sourceServiceName) { if (Array.isArray(layerInfo.fields)) { sourceLayerFields = sourceLayerFields.concat(layerInfo.fields.map((f) => f.name)); } else { sourceLayerFields = sourceLayerFields.concat(Object.keys(layerInfo.fields)); } } }); return sourceLayerFields; } } /** * Get a list of any dynamically calculated fields * These fields are still valid but will not exist in the source service * * @param table the table instance to compare * * @returns an array of field names * @private */ export function _getDynamicFieldNames(table) { const fieldNames = table.sourceLayerFields.reduce((prev, cur) => { if (cur.statisticType) { prev.push(cur.name); } return prev; }, []); return [...new Set(fieldNames)]; } /** * Remove fields references from fields and indexes that do not exist in the source service * * @param item Layer or table * @param templateDictionary Hash mapping Solution source id to id of its clone * * @returns updated layer or table * @private */ export function _updateItemFields(item, fieldNames) { /* istanbul ignore else */ if (fieldNames.length > 0) { /* istanbul ignore else */ if (item.fields) { item.fields = item.fields.filter((f) => fieldNames.indexOf(f.name) > -1); } /* istanbul ignore else */ if (item.indexes) { item.indexes = item.indexes.filter((f) => fieldNames.indexOf(f.fields) > -1); } } return item; } /** * Filter the sourceLayerFields for the table * * @param table the table instance to evaluate * @param sourceLayerFields array of fields from the source service * @returns Updated instance of the table * @private */ export function _updateSourceLayerFields(table, sourceLayerFields) { /* istanbul ignore else */ if (Array.isArray(table.sourceLayerFields) && table.sourceLayerFields.length > 0) { // need to make sure these actually exist in the source.. /* istanbul ignore else */ if (sourceLayerFields.length > 0) { setProp(table, "sourceLayerFields", table.sourceLayerFields.filter((f) => sourceLayerFields.indexOf(f.source.toLowerCase()) > -1)); } } return table; } /** * When the itemm is a view with a geometry field update the value to * use the table name from the view layer def * * @param item the item details from the current template * @param templateDictionary Hash mapping property names to replacement values * @private */ export function _updateGeomFieldName(adminLayerInfo, templateDictionary) { // issue #471 const tableName = getProp(adminLayerInfo, "viewLayerDefinition.table.name"); const fieldName = getProp(adminLayerInfo, "geometryField.name"); /* istanbul ignore else */ if (fieldName && tableName) { const geomName = templateDictionary.isPortal ? `${tableName}.shape` : `${tableName}.Shape`; setProp(adminLayerInfo, "geometryField.name", geomName); } else if (!fieldName && getProp(adminLayerInfo, "geometryField")) { // null geom field will cause failure to deploy in portal // this is also checked and removed on deploy for older solutions deleteProp(adminLayerInfo, "geometryField"); } } /** * Add the fields to the templateDictionary when a service has views * these are used to compare with fields from the view when domains are involved * when a view field has a domain that differs from that of the source service * the definition needs to be modified in an update call rather than when it is first added. * This should only happen when the domain differs. * * @param itemTemplate * @param templateDictionary Hash mapping Solution source id to id of its clone (and name & URL for feature service) * @private */ export function _updateTemplateDictionaryFields(itemTemplate, templateDictionary, compareItemId = true) { const layers = itemTemplate.properties.layers; const tables = itemTemplate.properties.tables; const layersAndTables = layers.concat(tables); const fieldInfos = {}; layersAndTables.forEach((layerOrTable) => { fieldInfos[layerOrTable.id] = layerOrTable.fields; }); Object.keys(templateDictionary).some((k) => { if (compareItemId ? templateDictionary[k].itemId === itemTemplate.itemId : k === itemTemplate.itemId) { templateDictionary[k].fieldInfos = fieldInfos; return true; } else { return false; } }); } /** * Set the defaultSpatialReference variable with the services spatial reference. * If this item is a Feature Service that has child views then we will use this value * if one or more of the child views spatial reference differs from that of its parent. * * @param templateDictionary Hash mapping Solution source id to id of its clone (and name & URL for feature service) * @param itemId The source id for the item * @param spatialReference \{ wkid: 102100 \} for example * @private */ export function setDefaultSpatialReference(templateDictionary, itemId, spatialReference) { /* istanbul ignore else */ if (spatialReference) { setCreateProp(