@esri/solution-common
Version:
Provides general helper functions for @esri/solution.js.
1,147 lines • 104 kB
JavaScript
/** @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(