From 250a804de79d6d9d9110cea5f2b7e3d44abb77b3 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Fri, 8 Sep 2023 17:51:45 -0600 Subject: [PATCH 01/12] feat(hub-common): create IHubEditableContent settings schema with hosted downloads toggle affects: @esri/hub-common --- .../src/content/_internal/ContentSchema.ts | 8 +++- .../content/_internal/ContentUiSchemaEdit.ts | 6 +-- .../_internal/ContentUiSchemaSettings.ts | 45 +++++++++++++++++++ .../src/content/_internal/getPropertyMap.ts | 5 +++ .../internal/getEntityEditorSchemas.ts | 6 ++- .../src/core/types/IHubEditableContent.ts | 8 ++++ 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 packages/common/src/content/_internal/ContentUiSchemaSettings.ts diff --git a/packages/common/src/content/_internal/ContentSchema.ts b/packages/common/src/content/_internal/ContentSchema.ts index fe34878222b..daa4614ab3f 100644 --- a/packages/common/src/content/_internal/ContentSchema.ts +++ b/packages/common/src/content/_internal/ContentSchema.ts @@ -2,7 +2,10 @@ import { IConfigurationSchema } from "../../core"; import { HubItemEntitySchema } from "../../core/schemas/shared/HubItemEntitySchema"; export type ContentEditorType = (typeof ContentEditorTypes)[number]; -export const ContentEditorTypes = ["hub:content:edit"] as const; +export const ContentEditorTypes = [ + "hub:content:edit", + "hub:content:settings", +] as const; /** * defines the JSON schema for a Hub Content's editable fields @@ -14,5 +17,8 @@ export const ContentSchema: IConfigurationSchema = { licenseInfo: { type: "string", }, + hostedDownloads: { + type: "boolean", + }, }, } as IConfigurationSchema; diff --git a/packages/common/src/content/_internal/ContentUiSchemaEdit.ts b/packages/common/src/content/_internal/ContentUiSchemaEdit.ts index 2dfe78b30f4..81efd0194d2 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaEdit.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaEdit.ts @@ -1,10 +1,10 @@ -import { IUiSchema } from "../../core"; import { IArcGISContext } from "../../ArcGISContext"; -import { IHubContent } from "../../core/types"; import { getTagItems } from "../../core/schemas/internal/getTagItems"; import { getCategoryItems } from "../../core/schemas/internal/getCategoryItems"; import { getLocationExtent } from "../../core/schemas/internal/getLocationExtent"; import { getLocationOptions } from "../../core/schemas/internal/getLocationOptions"; +import { IHubEditableContent } from "../../core/types/IHubEditableContent"; +import { IUiSchema } from "../../core/schemas/types"; /** * @private @@ -14,7 +14,7 @@ import { getLocationOptions } from "../../core/schemas/internal/getLocationOptio */ export const buildUiSchema = async ( i18nScope: string, - entity: IHubContent, + entity: IHubEditableContent, context: IArcGISContext ): Promise => { return { diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts new file mode 100644 index 00000000000..646076247f2 --- /dev/null +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -0,0 +1,45 @@ +import { IArcGISContext } from "../../ArcGISContext"; +import { IHubEditableContent, IUiSchema } from "../../core"; + +/** + * @private + * constructs the complete settings uiSchema for Hub Editable Content. + * This defines how the schema properties should be + * rendered in the content settings editing experience + */ + export const buildUiSchema = async ( + i18nScope: string, + entity: IHubEditableContent, + _context: IArcGISContext +): Promise => { + const uiSchema: IUiSchema = { + type: "Layout", + elements: [], + } + // TODO: Use helper + if (entity.type === 'Feature Service' && entity.typeKeywords?.includes('Hosted Service')) { + uiSchema.elements.push({ + type: "Section", + labelKey: `${i18nScope}.sections.downloads.label`, + options: { + helperText: { + labelKey: `${i18nScope}.sections.downloads.helperText`, + }, + }, + elements: [ + { + labelKey: `${i18nScope}.fields.hostedDownloads.label`, + scope: "/properties/hostedDownloads", + type: "Control", + options: { + helperText: { + labelKey: `${i18nScope}.fields.hostedDownloads.helperText`, + placement: "bottom", + }, + }, + } + ], + },) + } + return uiSchema; +}; diff --git a/packages/common/src/content/_internal/getPropertyMap.ts b/packages/common/src/content/_internal/getPropertyMap.ts index 63a0a0c6f19..bbe92e847c8 100644 --- a/packages/common/src/content/_internal/getPropertyMap.ts +++ b/packages/common/src/content/_internal/getPropertyMap.ts @@ -30,6 +30,11 @@ export function getPropertyMap(): IPropertyMap[] { storeKey: "item.licenseInfo", }); + map.push({ + entityKey: "hostedDownloads", + storeKey: "item.properties.downloads.hosted", + }); + // features is intentionally left out // TODO: look into composeContent() for what we can add here diff --git a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts index bd9222332b9..b93076be7dc 100644 --- a/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts +++ b/packages/common/src/core/schemas/internal/getEntityEditorSchemas.ts @@ -12,8 +12,8 @@ import { GroupEditorType } from "../../../groups/_internal/GroupSchema"; import { ConfigurableEntity } from "./ConfigurableEntity"; import { IArcGISContext } from "../../../ArcGISContext"; import { - IHubContent, IHubDiscussion, + IHubEditableContent, IHubGroup, IHubInitiative, IHubPage, @@ -151,10 +151,12 @@ export async function getEntityEditorSchemas( const contentModule = await { "hub:content:edit": () => import("../../../content/_internal/ContentUiSchemaEdit"), + "hub:content:settings": () => + import("../../../content/_internal/ContentUiSchemaSettings"), }[type as ContentEditorType](); uiSchema = await contentModule.buildUiSchema( i18nScope, - entity as IHubContent, + entity as IHubEditableContent, context ); diff --git a/packages/common/src/core/types/IHubEditableContent.ts b/packages/common/src/core/types/IHubEditableContent.ts index 9ddc9b04dc6..9d71bd2ce62 100644 --- a/packages/common/src/core/types/IHubEditableContent.ts +++ b/packages/common/src/core/types/IHubEditableContent.ts @@ -13,6 +13,14 @@ export interface IHubEditableContent * perhaps using Pick from IHubContent */ licenseInfo: string; + /** + * Indicates whether an item has opted into the hosted downloads experience + * + * NOTE: even if an item has opted into the hosted downloads experience, only items + * that meet specific criteria will actually see the hosted experience on the live view + * (i.e., the item is a Hosted Feature Service with the Extract capability enabled). + */ + hostedDownloads?: boolean; } export type IHubContentEditor = Omit & { From 5570d034bdb6d8de08d9087c31574ffe1ef6369a Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Mon, 11 Sep 2023 15:42:56 -0600 Subject: [PATCH 02/12] feat(hub-common): add rest.js service admin package affects: @esri/hub-common --- package.json | 1 + packages/common/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index c7ea6e94a1a..4d366ed686d 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@esri/arcgis-rest-feature-layer": "^3.4.3", "@esri/arcgis-rest-portal": "^3.5.0", "@esri/arcgis-rest-request": "^3.1.1", + "@esri/arcgis-rest-service-admin": "^3.4.3", "@esri/arcgis-rest-types": "^3.1.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", diff --git a/packages/common/package.json b/packages/common/package.json index 544e4106f27..273194c81c2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,6 +25,7 @@ "@esri/arcgis-rest-feature-layer": "^3.2.0", "@esri/arcgis-rest-portal": "^2.18.0 || 3", "@esri/arcgis-rest-request": "^2.14.0 || 3", + "@esri/arcgis-rest-service-admin": "^3.2.0", "@esri/arcgis-rest-types": "^2.15.0 || 3" }, "devDependencies": { From da3ba5a76709708d4bb03971feb328b369e0f9e4 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Mon, 11 Sep 2023 15:45:16 -0600 Subject: [PATCH 03/12] feat(hub-common): add server extract capability to content settings schema and fetching / updating logic affects: @esri/hub-common --- .../src/content/_internal/ContentSchema.ts | 3 + .../_internal/ContentUiSchemaSettings.ts | 26 +++- .../src/content/_internal/computeProps.ts | 30 ++++- packages/common/src/content/edit.ts | 122 ++++++++++++++---- packages/common/src/content/fetch.ts | 23 +++- .../src/core/types/IHubEditableContent.ts | 5 + 6 files changed, 172 insertions(+), 37 deletions(-) diff --git a/packages/common/src/content/_internal/ContentSchema.ts b/packages/common/src/content/_internal/ContentSchema.ts index daa4614ab3f..74fa5c5abc4 100644 --- a/packages/common/src/content/_internal/ContentSchema.ts +++ b/packages/common/src/content/_internal/ContentSchema.ts @@ -17,6 +17,9 @@ export const ContentSchema: IConfigurationSchema = { licenseInfo: { type: "string", }, + serverExtractCapability: { + type: "boolean", + }, hostedDownloads: { type: "boolean", }, diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts index 646076247f2..f788ccb7458 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -1,5 +1,6 @@ import { IArcGISContext } from "../../ArcGISContext"; -import { IHubEditableContent, IUiSchema } from "../../core"; +import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; +import { IHubEditableContent } from "../../core/types/IHubEditableContent"; /** * @private @@ -27,6 +28,17 @@ import { IHubEditableContent, IUiSchema } from "../../core"; }, }, elements: [ + { + labelKey: `${i18nScope}.fields.serverExtractCapability.label`, + scope: "/properties/serverExtractCapability", + type: "Control", + options: { + helperText: { + labelKey: + `${i18nScope}.fields.serverExtractCapability.helperText`, + }, + }, + }, { labelKey: `${i18nScope}.fields.hostedDownloads.label`, scope: "/properties/hostedDownloads", @@ -37,9 +49,19 @@ import { IHubEditableContent, IUiSchema } from "../../core"; placement: "bottom", }, }, + rule: { + effect: UiSchemaRuleEffects.DISABLE, + condition: { + schema: { + properties: { + serverExtractCapability: { const: false }, + }, + }, + }, + }, } ], - },) + }) } return uiSchema; }; diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index 07fa1c7d09e..7176a26560b 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -3,13 +3,14 @@ import { UserSession } from "@esri/arcgis-rest-auth"; import { getItemThumbnailUrl } from "../../resources"; import { IModel } from "../../types"; import { bBoxToExtent, extentToPolygon, isBBox } from "../../extent"; -import { IExtent } from "@esri/arcgis-rest-types"; +import { IExtent, IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; import Geometry = __esri.Geometry; import { getItemHomeUrl } from "../../urls/get-item-home-url"; import { getHubRelativeUrl } from "./internalContentUtils"; import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; +import { EnrichmentMap } from "../fetch"; // if called and valid, set 3 things -- else just return type custom export const getItemExtent = (itemExtent: number[][]): IExtent => { @@ -36,7 +37,8 @@ export function deriveLocationFromItemExtent(itemExtent?: number[][]) { export function computeProps( model: IModel, content: Partial, - requestOptions: IRequestOptions + requestOptions: IRequestOptions, + enrichments: EnrichmentMap = {} ): IHubEditableContent { let token: string; if (requestOptions.authentication) { @@ -70,5 +72,29 @@ export function computeProps( : deriveLocationFromItemExtent(model.item.extent); } + if (enrichments.server) { + content.serverExtractCapability = hasCapability( + "Extract", + enrichments.server + ); + } + return content as IHubEditableContent; } + +// TODO: Move this util elsewhere + +/** + * Returns a whether a service has a capability + * @param {string} capability + * @param {Partial} serviceDefinition + * + * @returns {boolean} + */ +export function hasCapability( + capability: string, + serverDefinition: Partial +) { + const capabilities = (serverDefinition.capabilities || "").split(","); + return capabilities.includes(capability); +} diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 3b566ddabe5..8bae9aa5f88 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -19,8 +19,14 @@ import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { cloneObject } from "../util"; import { IModel } from "../types"; -import { computeProps } from "./_internal/computeProps"; +import { hasCapability } from "./_internal/computeProps"; import { getProp } from "../objects/get-prop"; +import { EnrichmentMap, modelToHubEditableContent } from "./fetch"; +import { + getService, + IFeatureServiceDefinition, +} from "@esri/arcgis-rest-feature-layer"; +import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; // TODO: move this to defaults? const DEFAULT_CONTENT_MODEL: IModel = { @@ -90,13 +96,11 @@ export async function updateContent( content: IHubEditableContent, requestOptions: IUserRequestOptions ): Promise { - // let resources; - - // get the backing item & data - // We can't just call getModel because we need to be able + // Get the backing item + // NOTE: We can't just call `getMode`l because we need to be able // to properly handle other types like PDFs that don't have JSON data const item = await getItem(content.id, requestOptions); - const model = { item }; + const model: IModel = { item }; // create the PropertyMapper const mapper = new PropertyMapper, IModel>( getPropertyMap() @@ -111,30 +115,92 @@ export async function updateContent( modelToUpdate.item.properties.boundary = locationType === "none" ? "none" : "item"; - // TODO: if we have resources disconnect them from the model for now. - // if (modelToUpdate.resources) { - // resources = configureBaseResources( - // cloneObject(modelToUpdate.resources), - // EntityResourceMap - // ); - // delete modelToUpdate.resources; - // } // update the backing item const updatedModel = await updateModel(modelToUpdate, requestOptions); - // // if we have resources, create them, then re-attach them to the model - // if (resources) { - // updatedModel = await upsertModelResources( - // updatedModel, - // resources, - // requestOptions - // ); - // } - // now map back into a project and return that - let updatedContent = mapper.storeToEntity(updatedModel, content); - updatedContent = computeProps(model, updatedContent, requestOptions); - // the casting is needed because modelToObject returns a `Partial` - // where as this function returns a `T` - return updatedContent as IHubEditableContent; + + // update enrichment values + const enrichments: EnrichmentMap = {}; + if ( + isHostedFeatureService(content) && + content.serverExtractCapability != null + ) { + const currentDefinition = await getService({ + ...requestOptions, + url: content.url, + }); + const currentServerExtractEnabled = hasCapability( + "Extract", + currentDefinition + ); + if (currentServerExtractEnabled !== content.serverExtractCapability) { + const updatedDefinition = toggleFeatureServiceCapability( + "Extract", + currentDefinition + ); + await updateServiceDefinition(content.url, { + authentication: requestOptions.authentication, + updateDefinition: updatedDefinition, + }); + enrichments.server = updatedDefinition; + } + } + + return modelToHubEditableContent(updatedModel, requestOptions, enrichments); +} + +// TODO: Move this +function isHostedFeatureService(content: IHubEditableContent): boolean { + return ( + content.type === "Feature Service" && + content.typeKeywords.includes("Hosted Service") + ); +} + +/** + * TODO: MOVE THIS + * Toggles a single capability on a given feature service. + * Returns a service definition object with updated capabilities + * @param {string} capability capability to toggle + * @param {string} serviceUrl url of service to modify + * @param {IServiceDefinition} serviceDefinition current definition of the service + * + * @returns {IServiceDefinition} updated definition + */ +export function toggleFeatureServiceCapability( + capability: string, + serviceDefinition: Partial +): Partial { + const updatedDefinition = hasCapability(capability, serviceDefinition) + ? removeCapability(capability, serviceDefinition) + : addCapability(capability, serviceDefinition); + + return updatedDefinition; +} + +// TODO: Move this +function addCapability( + capability: string, + serverDefinition: Partial +): Partial { + const capabilities = (serverDefinition.capabilities || "") + .split(",") + .concat(capability) + .join(","); + const updated = { ...serverDefinition, capabilities }; + return updated; +} + +// TODO: Move this +export function removeCapability( + capability: string, + serverDefinition: Partial +): Partial { + const capabilities = (serverDefinition.capabilities || "") + .split(",") + .filter((c) => c !== capability) + .join(","); + const updated = { ...serverDefinition, capabilities }; + return updated; } /** diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index cf7d5056031..be0b2d24eed 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -241,16 +241,29 @@ export const fetchHubContent = async ( // TODO: fetch data? org? ownerUser? metadata? // could use getItemEnrichments() and remove server, layers, etc // but we'd have to do that _after_ fetching the item first - const enrichments = [] as ItemOrServerEnrichment[]; - const options = { ...requestOptions, enrichments } as IFetchContentOptions; + const composeEnrichments = ["server"] as ItemOrServerEnrichment[]; + const options = { + ...requestOptions, + enrichments: composeEnrichments, + } as IFetchContentOptions; // for now we call fetchContent(), which returns a superset of IHubEditableContent // in the long run we probably want to replace this w/ fetchItemAndEnrichments() // and then use the property mapper and computeProps() to compose the object const model = await fetchContent(identifier, options); - // for now we still need property mapper to get defaults not set by composeContent() + const enrichments: EnrichmentMap = { server: model.server }; + return modelToHubEditableContent(model, requestOptions, enrichments); +}; + +// TODO: Move elsewhere +export type EnrichmentMap = Partial>; +export function modelToHubEditableContent( + model: IModel, + requestOptions: IRequestOptions, + enrichments: EnrichmentMap = {} +) { const mapper = new PropertyMapper, IModel>( getPropertyMap() ); const content = mapper.storeToEntity(model, {}) as IHubEditableContent; - return computeProps(model, content, requestOptions); -}; + return computeProps(model, content, requestOptions, enrichments); +} diff --git a/packages/common/src/core/types/IHubEditableContent.ts b/packages/common/src/core/types/IHubEditableContent.ts index 9d71bd2ce62..077db16afe9 100644 --- a/packages/common/src/core/types/IHubEditableContent.ts +++ b/packages/common/src/core/types/IHubEditableContent.ts @@ -13,6 +13,11 @@ export interface IHubEditableContent * perhaps using Pick from IHubContent */ licenseInfo: string; + /** + * If the item represents a service, shows whether the service has the "Extract" + * capability enabled. This is a pre-requisite for Hosted Downloads to work. + */ + serverExtractCapability?: boolean; /** * Indicates whether an item has opted into the hosted downloads experience * From 770a7b613a092856ddf5b715c97b948c438f35ee Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Tue, 12 Sep 2023 08:45:00 -0600 Subject: [PATCH 04/12] refactor(hub-common): change fetchHubContent to get the model directly instead of using fetchContent affects: @esri/hub-common --- .../_internal/ContentUiSchemaSettings.ts | 16 ++++----- packages/common/src/content/edit.ts | 13 +++++--- packages/common/src/content/fetch.ts | 33 ++++++++----------- 3 files changed, 29 insertions(+), 33 deletions(-) diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts index f788ccb7458..eabe09d7e9e 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -1,6 +1,7 @@ import { IArcGISContext } from "../../ArcGISContext"; import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; +import { isHostedFeatureService } from "../edit"; /** * @private @@ -8,7 +9,7 @@ import { IHubEditableContent } from "../../core/types/IHubEditableContent"; * This defines how the schema properties should be * rendered in the content settings editing experience */ - export const buildUiSchema = async ( +export const buildUiSchema = async ( i18nScope: string, entity: IHubEditableContent, _context: IArcGISContext @@ -16,9 +17,8 @@ import { IHubEditableContent } from "../../core/types/IHubEditableContent"; const uiSchema: IUiSchema = { type: "Layout", elements: [], - } - // TODO: Use helper - if (entity.type === 'Feature Service' && entity.typeKeywords?.includes('Hosted Service')) { + }; + if (isHostedFeatureService(entity)) { uiSchema.elements.push({ type: "Section", labelKey: `${i18nScope}.sections.downloads.label`, @@ -34,8 +34,7 @@ import { IHubEditableContent } from "../../core/types/IHubEditableContent"; type: "Control", options: { helperText: { - labelKey: - `${i18nScope}.fields.serverExtractCapability.helperText`, + labelKey: `${i18nScope}.fields.serverExtractCapability.helperText`, }, }, }, @@ -46,7 +45,6 @@ import { IHubEditableContent } from "../../core/types/IHubEditableContent"; options: { helperText: { labelKey: `${i18nScope}.fields.hostedDownloads.helperText`, - placement: "bottom", }, }, rule: { @@ -59,9 +57,9 @@ import { IHubEditableContent } from "../../core/types/IHubEditableContent"; }, }, }, - } + }, ], - }) + }); } return uiSchema; }; diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 8bae9aa5f88..47dadf31750 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -4,6 +4,7 @@ import { IUserItemOptions, getItem, removeItem, + IItem, } from "@esri/arcgis-rest-portal"; import { IHubContentEditor, IHubEditableContent } from "../core"; @@ -120,10 +121,7 @@ export async function updateContent( // update enrichment values const enrichments: EnrichmentMap = {}; - if ( - isHostedFeatureService(content) && - content.serverExtractCapability != null - ) { + if (isHostedFeatureService(content)) { const currentDefinition = await getService({ ...requestOptions, url: content.url, @@ -132,6 +130,7 @@ export async function updateContent( "Extract", currentDefinition ); + // To avoid over-updating the service, we only fire an update call if Extract has changed if (currentServerExtractEnabled !== content.serverExtractCapability) { const updatedDefinition = toggleFeatureServiceCapability( "Extract", @@ -142,6 +141,8 @@ export async function updateContent( updateDefinition: updatedDefinition, }); enrichments.server = updatedDefinition; + } else { + enrichments.server = currentDefinition; } } @@ -149,7 +150,9 @@ export async function updateContent( } // TODO: Move this -function isHostedFeatureService(content: IHubEditableContent): boolean { +export function isHostedFeatureService( + content: IHubEditableContent | IItem +): boolean { return ( content.type === "Feature Service" && content.typeKeywords.includes("Hosted Service") diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index be0b2d24eed..24c056dcc2f 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -1,5 +1,6 @@ import { getLayer, + getService, parseServiceUrl, queryFeatures, } from "@esri/arcgis-rest-feature-layer"; @@ -20,16 +21,13 @@ import { getContentEnrichments, } from "./_fetch"; import { canUseHubApiForItem } from "./_internal/internalContentUtils"; -import { - composeContent, - getItemLayer, - getProxyUrl, - isLayerView, -} from "./compose"; +import { composeContent, getItemLayer, getProxyUrl } from "./compose"; import { IRequestOptions } from "@esri/arcgis-rest-request"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; +import { getModel } from "../models"; +import { isHostedFeatureService } from "./edit"; const hasFeatures = (contentType: string) => ["Feature Layer", "Table"].includes(contentType); @@ -238,19 +236,16 @@ export const fetchHubContent = async ( identifier: string, requestOptions: IRequestOptions ): Promise => { - // TODO: fetch data? org? ownerUser? metadata? - // could use getItemEnrichments() and remove server, layers, etc - // but we'd have to do that _after_ fetching the item first - const composeEnrichments = ["server"] as ItemOrServerEnrichment[]; - const options = { - ...requestOptions, - enrichments: composeEnrichments, - } as IFetchContentOptions; - // for now we call fetchContent(), which returns a superset of IHubEditableContent - // in the long run we probably want to replace this w/ fetchItemAndEnrichments() - // and then use the property mapper and computeProps() to compose the object - const model = await fetchContent(identifier, options); - const enrichments: EnrichmentMap = { server: model.server }; + const model = await getModel(identifier, requestOptions); + + const enrichments: EnrichmentMap = {}; + if (isHostedFeatureService(model.item)) { + enrichments.server = await getService({ + ...requestOptions, + url: model.item.url, + }); + } + return modelToHubEditableContent(model, requestOptions, enrichments); }; From 713734a1c2e7f64c030b378e409a467d33dba99b Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Tue, 12 Sep 2023 13:50:06 -0600 Subject: [PATCH 05/12] fix(hub-common): change to the proper version of @esri/arcgis-rest-service-admin affects: @esri/hub-common --- package-lock.json | 85 ++++++++++++++++++++++++++---------- package.json | 2 +- packages/common/package.json | 2 +- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3ea65d2b117..8afbb3cbceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@esri/arcgis-rest-feature-layer": "^3.4.3", "@esri/arcgis-rest-portal": "^3.5.0", "@esri/arcgis-rest-request": "^3.1.1", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^3.1.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", @@ -4993,10 +4994,24 @@ "tslib": "^1.10.0" } }, + "node_modules/@esri/arcgis-rest-service-admin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-service-admin/-/arcgis-rest-service-admin-3.7.0.tgz", + "integrity": "sha512-vPm+hyO8lPH9aO1PMgveGevKbiViXgbpsetG3BLruxrxEhT0B+QytWE/LFp1beZYILTrm6umZqacEi6T2KJ7Aw==", + "dependencies": { + "@esri/arcgis-rest-types": "^3.7.0", + "tslib": "^1.13.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-auth": "^3.0.0", + "@esri/arcgis-rest-portal": "^3.0.0", + "@esri/arcgis-rest-request": "^3.0.0" + } + }, "node_modules/@esri/arcgis-rest-types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.6.0.tgz", - "integrity": "sha512-t6QWdVNmqB9OloYAvVWYNjvqlnrXs/m0nCRNwPGt3ZiAPXn5CpkpSn2UD6LPqGp6vPxKG81lbFQvwCw9az4qIg==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.7.0.tgz", + "integrity": "sha512-KEgORx0HKHOrV4oMYOwmZ76N89WTNkbKb1z3UYJrOEaKVGRU3jisgQcuTXFqjJJe4ZApGQhxCzNgcaU067qdpA==" }, "node_modules/@esri/hub-common": { "resolved": "packages/common", @@ -22644,9 +22659,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseindexof": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseuniq": { "version": "4.6.0", @@ -22661,21 +22677,24 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._bindcallback": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._cacheindexof": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._createcache": { "version": "3.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "dependencies": { "lodash._getnative": "^3.0.0" } @@ -22689,9 +22708,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._getnative": { "version": "3.9.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._root": { "version": "3.0.1", @@ -22709,9 +22729,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.restparam": { "version": "3.6.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.union": { "version": "4.6.0", @@ -64966,7 +64987,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "14.11.0", + "version": "14.12.0", "license": "Apache-2.0", "dependencies": { "abab": "^2.0.5", @@ -64986,6 +65007,7 @@ "@esri/arcgis-rest-feature-layer": "^3.2.0", "@esri/arcgis-rest-portal": "^2.18.0 || 3", "@esri/arcgis-rest-request": "^2.14.0 || 3", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^2.15.0 || 3" } }, @@ -68688,10 +68710,19 @@ "tslib": "^1.10.0" } }, + "@esri/arcgis-rest-service-admin": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-service-admin/-/arcgis-rest-service-admin-3.7.0.tgz", + "integrity": "sha512-vPm+hyO8lPH9aO1PMgveGevKbiViXgbpsetG3BLruxrxEhT0B+QytWE/LFp1beZYILTrm6umZqacEi6T2KJ7Aw==", + "requires": { + "@esri/arcgis-rest-types": "^3.7.0", + "tslib": "^1.13.0" + } + }, "@esri/arcgis-rest-types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.6.0.tgz", - "integrity": "sha512-t6QWdVNmqB9OloYAvVWYNjvqlnrXs/m0nCRNwPGt3ZiAPXn5CpkpSn2UD6LPqGp6vPxKG81lbFQvwCw9az4qIg==" + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.7.0.tgz", + "integrity": "sha512-KEgORx0HKHOrV4oMYOwmZ76N89WTNkbKb1z3UYJrOEaKVGRU3jisgQcuTXFqjJJe4ZApGQhxCzNgcaU067qdpA==" }, "@esri/hub-common": { "version": "file:packages/common", @@ -83376,7 +83407,8 @@ "lodash._baseindexof": { "version": "3.1.0", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -83391,17 +83423,20 @@ "lodash._bindcallback": { "version": "3.0.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._cacheindexof": { "version": "3.0.2", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._createcache": { "version": "3.1.2", "bundled": true, - "extraneous": true, + "dev": true, + "peer": true, "requires": { "lodash._getnative": "^3.0.0" } @@ -83415,7 +83450,8 @@ "lodash._getnative": { "version": "3.9.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._root": { "version": "3.0.1", @@ -83432,7 +83468,8 @@ "lodash.restparam": { "version": "3.6.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash.union": { "version": "4.6.0", diff --git a/package.json b/package.json index 4d366ed686d..3e8f916a5e7 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@esri/arcgis-rest-feature-layer": "^3.4.3", "@esri/arcgis-rest-portal": "^3.5.0", "@esri/arcgis-rest-request": "^3.1.1", - "@esri/arcgis-rest-service-admin": "^3.4.3", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^3.1.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", diff --git a/packages/common/package.json b/packages/common/package.json index 273194c81c2..f2445f5087c 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,7 +25,7 @@ "@esri/arcgis-rest-feature-layer": "^3.2.0", "@esri/arcgis-rest-portal": "^2.18.0 || 3", "@esri/arcgis-rest-request": "^2.14.0 || 3", - "@esri/arcgis-rest-service-admin": "^3.2.0", + "@esri/arcgis-rest-service-admin": "^3.6.0", "@esri/arcgis-rest-types": "^2.15.0 || 3" }, "devDependencies": { From f787abe3fa5914be60a40b7b5dd0fbd4ed0abbc6 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Tue, 12 Sep 2023 14:24:04 -0600 Subject: [PATCH 06/12] refactor(hub-common): move utils and rename interfaces affects: @esri/hub-common --- .../src/content/_internal/computeProps.ts | 26 ++---- .../content/_internal/hostedServiceUtils.ts | 75 +++++++++++++++++ packages/common/src/content/edit.ts | 84 ++++--------------- packages/common/src/content/fetch.ts | 9 +- packages/common/src/items/_enrichments.ts | 2 + 5 files changed, 100 insertions(+), 96 deletions(-) create mode 100644 packages/common/src/content/_internal/hostedServiceUtils.ts diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index 7176a26560b..f4ea909a484 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -3,14 +3,15 @@ import { UserSession } from "@esri/arcgis-rest-auth"; import { getItemThumbnailUrl } from "../../resources"; import { IModel } from "../../types"; import { bBoxToExtent, extentToPolygon, isBBox } from "../../extent"; -import { IExtent, IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; +import { IExtent } from "@esri/arcgis-rest-types"; import Geometry = __esri.Geometry; import { getItemHomeUrl } from "../../urls/get-item-home-url"; import { getHubRelativeUrl } from "./internalContentUtils"; import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; -import { EnrichmentMap } from "../fetch"; +import { hasCapability, ServiceCapabilities } from "./hostedServiceUtils"; +import { IItemAndIServerEnrichments } from "../../items/_enrichments"; // if called and valid, set 3 things -- else just return type custom export const getItemExtent = (itemExtent: number[][]): IExtent => { @@ -38,7 +39,7 @@ export function computeProps( model: IModel, content: Partial, requestOptions: IRequestOptions, - enrichments: EnrichmentMap = {} + enrichments: IItemAndIServerEnrichments = {} ): IHubEditableContent { let token: string; if (requestOptions.authentication) { @@ -74,27 +75,10 @@ export function computeProps( if (enrichments.server) { content.serverExtractCapability = hasCapability( - "Extract", + ServiceCapabilities.EXTRACT, enrichments.server ); } return content as IHubEditableContent; } - -// TODO: Move this util elsewhere - -/** - * Returns a whether a service has a capability - * @param {string} capability - * @param {Partial} serviceDefinition - * - * @returns {boolean} - */ -export function hasCapability( - capability: string, - serverDefinition: Partial -) { - const capabilities = (serverDefinition.capabilities || "").split(","); - return capabilities.includes(capability); -} diff --git a/packages/common/src/content/_internal/hostedServiceUtils.ts b/packages/common/src/content/_internal/hostedServiceUtils.ts new file mode 100644 index 00000000000..06af7c227ab --- /dev/null +++ b/packages/common/src/content/_internal/hostedServiceUtils.ts @@ -0,0 +1,75 @@ +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-feature-layer"; +import { IItem } from "@esri/arcgis-rest-portal"; +import { IHubEditableContent } from "../../core/types/IHubEditableContent"; + +export function isHostedFeatureService( + content: IHubEditableContent | IItem +): boolean { + return ( + content.type === "Feature Service" && + content.typeKeywords.includes("Hosted Service") + ); +} + +export enum ServiceCapabilities { + EXTRACT = "Extract", +} + +/** + * Returns a whether a service has a capability + * @param capability + * @param serviceDefinition + * + * @returns {boolean} + */ +export function hasCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +) { + const capabilities = (serviceDefinition.capabilities || "").split(","); + return capabilities.includes(capability); +} + +/** + * Toggles a single capability on a given feature service. + * Returns a service definition object with updated capabilities + * @param capability capability to toggle + * @param serviceUrl url of service to modify + * @param serviceDefinition current definition of the service + * + * @returns updated definition + */ +export function toggleCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const updatedDefinition = hasCapability(capability, serviceDefinition) + ? removeCapability(capability, serviceDefinition) + : addCapability(capability, serviceDefinition); + + return updatedDefinition; +} + +function addCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const capabilities = (serviceDefinition.capabilities || "") + .split(",") + .concat(capability) + .join(","); + const updated = { ...serviceDefinition, capabilities }; + return updated; +} + +export function removeCapability( + capability: ServiceCapabilities, + serviceDefinition: Partial +): Partial { + const capabilities = (serviceDefinition.capabilities || "") + .split(",") + .filter((c) => c !== capability) + .join(","); + const updated = { ...serviceDefinition, capabilities }; + return updated; +} diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 47dadf31750..e42d583cd8c 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -4,14 +4,12 @@ import { IUserItemOptions, getItem, removeItem, - IItem, } from "@esri/arcgis-rest-portal"; import { IHubContentEditor, IHubEditableContent } from "../core"; // Note - we separate these imports so we can cleanly spy on things in tests import { createModel, - getModel, updateModel, // upsertModelResources, } from "../models"; @@ -20,14 +18,17 @@ import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { cloneObject } from "../util"; import { IModel } from "../types"; -import { hasCapability } from "./_internal/computeProps"; import { getProp } from "../objects/get-prop"; -import { EnrichmentMap, modelToHubEditableContent } from "./fetch"; -import { - getService, - IFeatureServiceDefinition, -} from "@esri/arcgis-rest-feature-layer"; +import { modelToHubEditableContent } from "./fetch"; +import { getService } from "@esri/arcgis-rest-feature-layer"; import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; +import { + hasCapability, + isHostedFeatureService, + ServiceCapabilities, + toggleCapability, +} from "./_internal/hostedServiceUtils"; +import { IItemAndIServerEnrichments } from "../items/_enrichments"; // TODO: move this to defaults? const DEFAULT_CONTENT_MODEL: IModel = { @@ -98,7 +99,7 @@ export async function updateContent( requestOptions: IUserRequestOptions ): Promise { // Get the backing item - // NOTE: We can't just call `getMode`l because we need to be able + // NOTE: We can't just call `getModel` because we need to be able // to properly handle other types like PDFs that don't have JSON data const item = await getItem(content.id, requestOptions); const model: IModel = { item }; @@ -120,20 +121,20 @@ export async function updateContent( const updatedModel = await updateModel(modelToUpdate, requestOptions); // update enrichment values - const enrichments: EnrichmentMap = {}; + const enrichments: IItemAndIServerEnrichments = {}; if (isHostedFeatureService(content)) { const currentDefinition = await getService({ ...requestOptions, url: content.url, }); const currentServerExtractEnabled = hasCapability( - "Extract", + ServiceCapabilities.EXTRACT, currentDefinition ); // To avoid over-updating the service, we only fire an update call if Extract has changed if (currentServerExtractEnabled !== content.serverExtractCapability) { - const updatedDefinition = toggleFeatureServiceCapability( - "Extract", + const updatedDefinition = toggleCapability( + ServiceCapabilities.EXTRACT, currentDefinition ); await updateServiceDefinition(content.url, { @@ -149,63 +150,6 @@ export async function updateContent( return modelToHubEditableContent(updatedModel, requestOptions, enrichments); } -// TODO: Move this -export function isHostedFeatureService( - content: IHubEditableContent | IItem -): boolean { - return ( - content.type === "Feature Service" && - content.typeKeywords.includes("Hosted Service") - ); -} - -/** - * TODO: MOVE THIS - * Toggles a single capability on a given feature service. - * Returns a service definition object with updated capabilities - * @param {string} capability capability to toggle - * @param {string} serviceUrl url of service to modify - * @param {IServiceDefinition} serviceDefinition current definition of the service - * - * @returns {IServiceDefinition} updated definition - */ -export function toggleFeatureServiceCapability( - capability: string, - serviceDefinition: Partial -): Partial { - const updatedDefinition = hasCapability(capability, serviceDefinition) - ? removeCapability(capability, serviceDefinition) - : addCapability(capability, serviceDefinition); - - return updatedDefinition; -} - -// TODO: Move this -function addCapability( - capability: string, - serverDefinition: Partial -): Partial { - const capabilities = (serverDefinition.capabilities || "") - .split(",") - .concat(capability) - .join(","); - const updated = { ...serverDefinition, capabilities }; - return updated; -} - -// TODO: Move this -export function removeCapability( - capability: string, - serverDefinition: Partial -): Partial { - const capabilities = (serverDefinition.capabilities || "") - .split(",") - .filter((c) => c !== capability) - .join(","); - const updated = { ...serverDefinition, capabilities }; - return updated; -} - /** * @private * Remove a Hub Content diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index 24c056dcc2f..79688495eb8 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -10,6 +10,7 @@ import { ItemOrServerEnrichment, fetchItemEnrichments, IItemAndEnrichments, + IItemAndIServerEnrichments, } from "../items/_enrichments"; import { IHubRequestOptions, IModel } from "../types"; import { isNil } from "../util"; @@ -27,7 +28,7 @@ import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; import { getModel } from "../models"; -import { isHostedFeatureService } from "./edit"; +import { isHostedFeatureService } from "./_internal/hostedServiceUtils"; const hasFeatures = (contentType: string) => ["Feature Layer", "Table"].includes(contentType); @@ -238,7 +239,7 @@ export const fetchHubContent = async ( ): Promise => { const model = await getModel(identifier, requestOptions); - const enrichments: EnrichmentMap = {}; + const enrichments: IItemAndIServerEnrichments = {}; if (isHostedFeatureService(model.item)) { enrichments.server = await getService({ ...requestOptions, @@ -249,12 +250,10 @@ export const fetchHubContent = async ( return modelToHubEditableContent(model, requestOptions, enrichments); }; -// TODO: Move elsewhere -export type EnrichmentMap = Partial>; export function modelToHubEditableContent( model: IModel, requestOptions: IRequestOptions, - enrichments: EnrichmentMap = {} + enrichments: IItemAndIServerEnrichments = {} ) { const mapper = new PropertyMapper, IModel>( getPropertyMap() diff --git a/packages/common/src/items/_enrichments.ts b/packages/common/src/items/_enrichments.ts index 2def9728d30..359127be7f5 100644 --- a/packages/common/src/items/_enrichments.ts +++ b/packages/common/src/items/_enrichments.ts @@ -37,6 +37,8 @@ export type ItemOrServerEnrichment = | keyof IItemEnrichments | keyof IServerEnrichments; +export type IItemAndIServerEnrichments = IItemEnrichments & IServerEnrichments; + /** * Lazy load XML parsing library and parse metadata XML into JSON * @param metadataXml From bbe06e863bfb16b7bcdec036f964cc34528ffac1 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Tue, 12 Sep 2023 15:35:17 -0600 Subject: [PATCH 07/12] fix(hub-common): run content.url through "parseServiceUrl" before fetching / updating service affects: @esri/hub-common --- packages/common/src/content/edit.ts | 4 ++-- packages/common/src/content/fetch.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index e42d583cd8c..ef84ec6c1fa 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -20,7 +20,7 @@ import { cloneObject } from "../util"; import { IModel } from "../types"; import { getProp } from "../objects/get-prop"; import { modelToHubEditableContent } from "./fetch"; -import { getService } from "@esri/arcgis-rest-feature-layer"; +import { getService, parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; import { hasCapability, @@ -137,7 +137,7 @@ export async function updateContent( ServiceCapabilities.EXTRACT, currentDefinition ); - await updateServiceDefinition(content.url, { + await updateServiceDefinition(parseServiceUrl(content.url), { authentication: requestOptions.authentication, updateDefinition: updatedDefinition, }); diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index 79688495eb8..ce27dff48e4 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -243,7 +243,7 @@ export const fetchHubContent = async ( if (isHostedFeatureService(model.item)) { enrichments.server = await getService({ ...requestOptions, - url: model.item.url, + url: parseServiceUrl(model.item.url), }); } From d3c452733110eed7b90f77360e293f49ae923794 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Tue, 12 Sep 2023 15:56:44 -0600 Subject: [PATCH 08/12] fix(hub-common): don't fetch data.json for IHubEditableContent entries affects: @esri/hub-common --- .../_internal/ContentUiSchemaSettings.ts | 4 ++-- .../content/_internal/hostedServiceUtils.ts | 20 +++++++++++++------ packages/common/src/content/edit.ts | 4 ++-- packages/common/src/content/fetch.ts | 14 ++++++------- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts index eabe09d7e9e..b8de208b947 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -1,7 +1,7 @@ import { IArcGISContext } from "../../ArcGISContext"; import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; -import { isHostedFeatureService } from "../edit"; +import { isHostedFeatureServiceEntity } from "./hostedServiceUtils"; /** * @private @@ -18,7 +18,7 @@ export const buildUiSchema = async ( type: "Layout", elements: [], }; - if (isHostedFeatureService(entity)) { + if (isHostedFeatureServiceEntity(entity)) { uiSchema.elements.push({ type: "Section", labelKey: `${i18nScope}.sections.downloads.label`, diff --git a/packages/common/src/content/_internal/hostedServiceUtils.ts b/packages/common/src/content/_internal/hostedServiceUtils.ts index 06af7c227ab..0be5da88a95 100644 --- a/packages/common/src/content/_internal/hostedServiceUtils.ts +++ b/packages/common/src/content/_internal/hostedServiceUtils.ts @@ -2,13 +2,21 @@ import { IFeatureServiceDefinition } from "@esri/arcgis-rest-feature-layer"; import { IItem } from "@esri/arcgis-rest-portal"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; -export function isHostedFeatureService( - content: IHubEditableContent | IItem +export function isHostedFeatureServiceItem(item: IItem): boolean { + return isHostedFeatureService(item.type, item.typeKeywords); +} + +export function isHostedFeatureServiceEntity( + content: IHubEditableContent +): boolean { + return isHostedFeatureService(content.type, content.typeKeywords); +} + +function isHostedFeatureService( + type: string, + typeKeywords: string[] = [] ): boolean { - return ( - content.type === "Feature Service" && - content.typeKeywords.includes("Hosted Service") - ); + return type === "Feature Service" && typeKeywords.includes("Hosted Service"); } export enum ServiceCapabilities { diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index ef84ec6c1fa..f33f384e946 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -24,7 +24,7 @@ import { getService, parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; import { hasCapability, - isHostedFeatureService, + isHostedFeatureServiceEntity, ServiceCapabilities, toggleCapability, } from "./_internal/hostedServiceUtils"; @@ -122,7 +122,7 @@ export async function updateContent( // update enrichment values const enrichments: IItemAndIServerEnrichments = {}; - if (isHostedFeatureService(content)) { + if (isHostedFeatureServiceEntity(content)) { const currentDefinition = await getService({ ...requestOptions, url: content.url, diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index ce27dff48e4..7bb7fbd71d0 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -27,8 +27,7 @@ import { IRequestOptions } from "@esri/arcgis-rest-request"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; -import { getModel } from "../models"; -import { isHostedFeatureService } from "./_internal/hostedServiceUtils"; +import { isHostedFeatureServiceItem } from "./_internal/hostedServiceUtils"; const hasFeatures = (contentType: string) => ["Feature Layer", "Table"].includes(contentType); @@ -237,16 +236,17 @@ export const fetchHubContent = async ( identifier: string, requestOptions: IRequestOptions ): Promise => { - const model = await getModel(identifier, requestOptions); - + // NOTE: We can't just call `getModel` because we need to be able + // to properly handle other types like PDFs that don't have JSON data + const item = await getItem(identifier, requestOptions); const enrichments: IItemAndIServerEnrichments = {}; - if (isHostedFeatureService(model.item)) { + if (isHostedFeatureServiceItem(item)) { enrichments.server = await getService({ ...requestOptions, - url: parseServiceUrl(model.item.url), + url: parseServiceUrl(item.url), }); } - + const model: IModel = { item }; return modelToHubEditableContent(model, requestOptions, enrichments); }; From 17ec9164ace9619de68c490a89db5f3cfeea5262 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Wed, 13 Sep 2023 11:12:52 -0600 Subject: [PATCH 09/12] refactor(hub-common): change names of server capability utils for clarity affects: @esri/hub-common --- .../src/content/_internal/computeProps.ts | 7 +++++-- .../content/_internal/hostedServiceUtils.ts | 18 +++++++++++------- packages/common/src/content/edit.ts | 8 ++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index f4ea909a484..e32ee118213 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -10,7 +10,10 @@ import { getHubRelativeUrl } from "./internalContentUtils"; import { IHubLocation } from "../../core/types/IHubLocation"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; -import { hasCapability, ServiceCapabilities } from "./hostedServiceUtils"; +import { + hasServiceCapability, + ServiceCapabilities, +} from "./hostedServiceUtils"; import { IItemAndIServerEnrichments } from "../../items/_enrichments"; // if called and valid, set 3 things -- else just return type custom @@ -74,7 +77,7 @@ export function computeProps( } if (enrichments.server) { - content.serverExtractCapability = hasCapability( + content.serverExtractCapability = hasServiceCapability( ServiceCapabilities.EXTRACT, enrichments.server ); diff --git a/packages/common/src/content/_internal/hostedServiceUtils.ts b/packages/common/src/content/_internal/hostedServiceUtils.ts index 0be5da88a95..58394f9f487 100644 --- a/packages/common/src/content/_internal/hostedServiceUtils.ts +++ b/packages/common/src/content/_internal/hostedServiceUtils.ts @@ -16,6 +16,10 @@ function isHostedFeatureService( type: string, typeKeywords: string[] = [] ): boolean { + // This logic was given to us by the ArcGIS Online home app team. Apparently this is + // part of the check they internally do when deciding whether to show the "Export Data" + // button on the item settings page. See the "Tech" section of this issue for more details: + // https://devtopia.esri.com/dc/hub/issues/7210 return type === "Feature Service" && typeKeywords.includes("Hosted Service"); } @@ -30,7 +34,7 @@ export enum ServiceCapabilities { * * @returns {boolean} */ -export function hasCapability( +export function hasServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ) { @@ -47,18 +51,18 @@ export function hasCapability( * * @returns updated definition */ -export function toggleCapability( +export function toggleServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ): Partial { - const updatedDefinition = hasCapability(capability, serviceDefinition) - ? removeCapability(capability, serviceDefinition) - : addCapability(capability, serviceDefinition); + const updatedDefinition = hasServiceCapability(capability, serviceDefinition) + ? removeServiceCapability(capability, serviceDefinition) + : addServiceCapability(capability, serviceDefinition); return updatedDefinition; } -function addCapability( +function addServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ): Partial { @@ -70,7 +74,7 @@ function addCapability( return updated; } -export function removeCapability( +export function removeServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ): Partial { diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index f33f384e946..2429275d8bf 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -23,10 +23,10 @@ import { modelToHubEditableContent } from "./fetch"; import { getService, parseServiceUrl } from "@esri/arcgis-rest-feature-layer"; import { updateServiceDefinition } from "@esri/arcgis-rest-service-admin"; import { - hasCapability, + hasServiceCapability, isHostedFeatureServiceEntity, ServiceCapabilities, - toggleCapability, + toggleServiceCapability, } from "./_internal/hostedServiceUtils"; import { IItemAndIServerEnrichments } from "../items/_enrichments"; @@ -127,13 +127,13 @@ export async function updateContent( ...requestOptions, url: content.url, }); - const currentServerExtractEnabled = hasCapability( + const currentServerExtractEnabled = hasServiceCapability( ServiceCapabilities.EXTRACT, currentDefinition ); // To avoid over-updating the service, we only fire an update call if Extract has changed if (currentServerExtractEnabled !== content.serverExtractCapability) { - const updatedDefinition = toggleCapability( + const updatedDefinition = toggleServiceCapability( ServiceCapabilities.EXTRACT, currentDefinition ); From 1a1479d98021039e37f9db813a99df4451b0d08a Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Wed, 13 Sep 2023 13:42:28 -0600 Subject: [PATCH 10/12] feat(hub-common): expose hosted service util functions affects: @esri/hub-common --- .../src/content/_internal/ContentUiSchemaSettings.ts | 2 +- packages/common/src/content/_internal/computeProps.ts | 2 +- packages/common/src/content/edit.ts | 2 +- packages/common/src/content/fetch.ts | 2 +- .../src/content/{_internal => }/hostedServiceUtils.ts | 8 ++++---- packages/common/src/content/index.ts | 1 + 6 files changed, 9 insertions(+), 8 deletions(-) rename packages/common/src/content/{_internal => }/hostedServiceUtils.ts (93%) diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts index b8de208b947..35462f84159 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -1,7 +1,7 @@ import { IArcGISContext } from "../../ArcGISContext"; import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; -import { isHostedFeatureServiceEntity } from "./hostedServiceUtils"; +import { isHostedFeatureServiceEntity } from "../hostedServiceUtils"; /** * @private diff --git a/packages/common/src/content/_internal/computeProps.ts b/packages/common/src/content/_internal/computeProps.ts index e32ee118213..b3642d526e1 100644 --- a/packages/common/src/content/_internal/computeProps.ts +++ b/packages/common/src/content/_internal/computeProps.ts @@ -13,7 +13,7 @@ import { getRelativeWorkspaceUrl } from "../../core/getRelativeWorkspaceUrl"; import { hasServiceCapability, ServiceCapabilities, -} from "./hostedServiceUtils"; +} from "../hostedServiceUtils"; import { IItemAndIServerEnrichments } from "../../items/_enrichments"; // if called and valid, set 3 things -- else just return type custom diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 2429275d8bf..fda1a3f3bf0 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -27,7 +27,7 @@ import { isHostedFeatureServiceEntity, ServiceCapabilities, toggleServiceCapability, -} from "./_internal/hostedServiceUtils"; +} from "./hostedServiceUtils"; import { IItemAndIServerEnrichments } from "../items/_enrichments"; // TODO: move this to defaults? diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index 7bb7fbd71d0..145c403c178 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -27,7 +27,7 @@ import { IRequestOptions } from "@esri/arcgis-rest-request"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; -import { isHostedFeatureServiceItem } from "./_internal/hostedServiceUtils"; +import { isHostedFeatureServiceItem } from "./hostedServiceUtils"; const hasFeatures = (contentType: string) => ["Feature Layer", "Table"].includes(contentType); diff --git a/packages/common/src/content/_internal/hostedServiceUtils.ts b/packages/common/src/content/hostedServiceUtils.ts similarity index 93% rename from packages/common/src/content/_internal/hostedServiceUtils.ts rename to packages/common/src/content/hostedServiceUtils.ts index 58394f9f487..1c57e1da13e 100644 --- a/packages/common/src/content/_internal/hostedServiceUtils.ts +++ b/packages/common/src/content/hostedServiceUtils.ts @@ -1,6 +1,6 @@ import { IFeatureServiceDefinition } from "@esri/arcgis-rest-feature-layer"; import { IItem } from "@esri/arcgis-rest-portal"; -import { IHubEditableContent } from "../../core/types/IHubEditableContent"; +import { IHubEditableContent } from "../core/types/IHubEditableContent"; export function isHostedFeatureServiceItem(item: IItem): boolean { return isHostedFeatureService(item.type, item.typeKeywords); @@ -46,7 +46,6 @@ export function hasServiceCapability( * Toggles a single capability on a given feature service. * Returns a service definition object with updated capabilities * @param capability capability to toggle - * @param serviceUrl url of service to modify * @param serviceDefinition current definition of the service * * @returns updated definition @@ -68,19 +67,20 @@ function addServiceCapability( ): Partial { const capabilities = (serviceDefinition.capabilities || "") .split(",") + .filter((c) => !!c) .concat(capability) .join(","); const updated = { ...serviceDefinition, capabilities }; return updated; } -export function removeServiceCapability( +function removeServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ): Partial { const capabilities = (serviceDefinition.capabilities || "") .split(",") - .filter((c) => c !== capability) + .filter((c) => !!c && c !== capability) .join(","); const updated = { ...serviceDefinition, capabilities }; return updated; diff --git a/packages/common/src/content/index.ts b/packages/common/src/content/index.ts index ecc8875e532..ce9318942e0 100644 --- a/packages/common/src/content/index.ts +++ b/packages/common/src/content/index.ts @@ -9,3 +9,4 @@ export * from "./HubContent"; export * from "./search"; export * from "./slugs"; export * from "./types"; +export * from "./hostedServiceUtils"; From fd52d8161698033a6a13d995fd315b1403cc3d3d Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Wed, 13 Sep 2023 15:45:04 -0600 Subject: [PATCH 11/12] fix(hub-common): add some bug fixes and add missing tests affects: @esri/hub-common --- packages/common/src/content/fetch.ts | 2 +- .../common/src/content/hostedServiceUtils.ts | 4 +- .../_internal/ContentUiSchemaSettings.test.ts | 74 ++++++++++ .../common/test/content/computeProps.test.ts | 73 ++++++++- packages/common/test/content/edit.test.ts | 138 +++++++++++++++--- packages/common/test/content/fetch.test.ts | 89 +++++++++-- packages/common/test/content/fixtures.ts | 65 +++++++++ .../test/content/hostedServiceUtils.test.ts | 86 +++++++++++ .../internal/getEntityEditorSchemas.test.ts | 2 + 9 files changed, 491 insertions(+), 42 deletions(-) create mode 100644 packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts create mode 100644 packages/common/test/content/fixtures.ts create mode 100644 packages/common/test/content/hostedServiceUtils.test.ts diff --git a/packages/common/src/content/fetch.ts b/packages/common/src/content/fetch.ts index 145c403c178..c311932e5d6 100644 --- a/packages/common/src/content/fetch.ts +++ b/packages/common/src/content/fetch.ts @@ -253,7 +253,7 @@ export const fetchHubContent = async ( export function modelToHubEditableContent( model: IModel, requestOptions: IRequestOptions, - enrichments: IItemAndIServerEnrichments = {} + enrichments: IItemAndIServerEnrichments ) { const mapper = new PropertyMapper, IModel>( getPropertyMap() diff --git a/packages/common/src/content/hostedServiceUtils.ts b/packages/common/src/content/hostedServiceUtils.ts index 1c57e1da13e..028c66d7c45 100644 --- a/packages/common/src/content/hostedServiceUtils.ts +++ b/packages/common/src/content/hostedServiceUtils.ts @@ -78,9 +78,9 @@ function removeServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial ): Partial { - const capabilities = (serviceDefinition.capabilities || "") + const capabilities = serviceDefinition.capabilities .split(",") - .filter((c) => !!c && c !== capability) + .filter((c) => c !== capability) .join(","); const updated = { ...serviceDefinition, capabilities }; return updated; diff --git a/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts new file mode 100644 index 00000000000..b3b375f9f44 --- /dev/null +++ b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts @@ -0,0 +1,74 @@ +import { buildUiSchema } from "../../../src/content/_internal/ContentUiSchemaSettings"; +import { MOCK_CONTEXT } from "../../mocks/mock-auth"; +import * as hostedServiceUtilsModule from "../../../src/content/hostedServiceUtils"; +import { UiSchemaRuleEffects } from "../../../src/core/schemas/types"; + +describe("buildUiSchema: content settings", () => { + it("includes download fields for hosted feature service entities", async () => { + spyOn( + hostedServiceUtilsModule, + "isHostedFeatureServiceEntity" + ).and.returnValue(true); + + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "some.scope.sections.downloads.label", + options: { + helperText: { + labelKey: "some.scope.sections.downloads.helperText", + }, + }, + elements: [ + { + labelKey: "some.scope.fields.serverExtractCapability.label", + scope: "/properties/serverExtractCapability", + type: "Control", + options: { + helperText: { + labelKey: + "some.scope.fields.serverExtractCapability.helperText", + }, + }, + }, + { + labelKey: "some.scope.fields.hostedDownloads.label", + scope: "/properties/hostedDownloads", + type: "Control", + options: { + helperText: { + labelKey: "some.scope.fields.hostedDownloads.helperText", + }, + }, + rule: { + effect: UiSchemaRuleEffects.DISABLE, + condition: { + schema: { + properties: { + serverExtractCapability: { const: false }, + }, + }, + }, + }, + }, + ], + }, + ], + }); + }); + it("excludes download fields for other entities", async () => { + spyOn( + hostedServiceUtilsModule, + "isHostedFeatureServiceEntity" + ).and.returnValue(false); + + const uiSchema = await buildUiSchema("some.scope", {} as any, MOCK_CONTEXT); + expect(uiSchema).toEqual({ + type: "Layout", + elements: [], + }); + }); +}); diff --git a/packages/common/test/content/computeProps.test.ts b/packages/common/test/content/computeProps.test.ts index 08fa8f9959d..52735bfe50c 100644 --- a/packages/common/test/content/computeProps.test.ts +++ b/packages/common/test/content/computeProps.test.ts @@ -4,6 +4,7 @@ import { getItemExtent, } from "../../src/content/_internal/computeProps"; import { IHubEditableContent } from "../../src/core/types/IHubEditableContent"; +import { IItemAndIServerEnrichments } from "../../src/items/_enrichments"; import { IHubRequestOptions, IModel } from "../../src/types"; import { cloneObject } from "../../src/util"; import { MOCK_HUB_REQOPTS } from "../mocks/mock-auth"; @@ -14,7 +15,28 @@ describe("content computeProps", () => { requestOptions = cloneObject(MOCK_HUB_REQOPTS); }); - it("computeProps model boundary undefined", () => { + it("handles when properties are undefined", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + }, + // no boundary set + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + // no location set + }; + + const chk = computeProps(model, content, requestOptions); + + expect(chk.location?.type).toBe("custom"); + }); + + it("handles when boundary is undefined", () => { const model: IModel = { item: { type: "Feature Service", @@ -25,7 +47,6 @@ describe("content computeProps", () => { // nothing set in properties }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -39,7 +60,7 @@ describe("content computeProps", () => { expect(chk.location?.type).toBe("custom"); }); - it("computeProps boundary defined as none", () => { + it("handles when boundary defined as none", () => { const model: IModel = { item: { type: "Feature Service", @@ -50,7 +71,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -75,7 +95,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -100,7 +119,6 @@ describe("content computeProps", () => { boundary: "none", }, }, - data: {}, // no boundary set } as IModel; const content: Partial = { @@ -114,6 +132,49 @@ describe("content computeProps", () => { expect(chk.links.siteRelative).toBe("/maps/my-slug"); }); + + it("adds server based enrichments if available", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + properties: {}, + }, + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + }; + const enrichments: IItemAndIServerEnrichments = { + server: { capabilities: "Extract" }, + }; + + const chk = computeProps(model, content, requestOptions, enrichments); + expect(chk.serverExtractCapability).toBeTruthy(); + }); + + it("handles when authentication isn't defined", () => { + const model: IModel = { + item: { + type: "Feature Service", + id: "9001", + created: new Date().getTime(), + modified: new Date().getTime(), + properties: {}, + }, + } as IModel; + const content: Partial = { + type: "Feature Service", + id: "9001", + }; + const withoutAuth = cloneObject(requestOptions); + delete withoutAuth.authentication; + + const chk = computeProps(model, content, withoutAuth); + expect(chk.thumbnail).toBeUndefined(); + }); }); describe("getItemExtent", () => { diff --git a/packages/common/test/content/edit.test.ts b/packages/common/test/content/edit.test.ts index f13a464294d..9490920e320 100644 --- a/packages/common/test/content/edit.test.ts +++ b/packages/common/test/content/edit.test.ts @@ -1,7 +1,9 @@ import * as portalModule from "@esri/arcgis-rest-portal"; +import * as featureLayerModule from "@esri/arcgis-rest-feature-layer"; +import * as adminModule from "@esri/arcgis-rest-service-admin"; import { MOCK_AUTH } from "../mocks/mock-auth"; import * as modelUtils from "../../src/models"; -import { IHubRequestOptions, IModel } from "../../src/types"; +import { IModel } from "../../src/types"; import { IHubEditableContent } from "../../src/core/types"; import { createContent, @@ -38,19 +40,38 @@ describe("content editing:", () => { }); }); describe("update content:", () => { - it("converts to a model and updates the item", async () => { - const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + let getItemSpy: jasmine.Spy; + let getServiceSpy: jasmine.Spy; + let updateModelSpy: jasmine.Spy; + let updateServiceSpy: jasmine.Spy; + beforeEach(() => { + getItemSpy = spyOn(portalModule, "getItem").and.returnValue( Promise.resolve({ item: { typeKeywords: [], }, }) ); - const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( + getServiceSpy = spyOn(featureLayerModule, "getService"); + updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( (m: IModel) => { return Promise.resolve(m); } ); + updateServiceSpy = spyOn( + adminModule, + "updateServiceDefinition" + ).and.callFake((_url: string, opts: any) => + Promise.resolve(opts.updateDefinition) + ); + }); + afterEach(() => { + getItemSpy.calls.reset(); + getServiceSpy.calls.reset(); + updateModelSpy.calls.reset(); + updateServiceSpy.calls.reset(); + }); + it("converts to a model and updates the item", async () => { const content: IHubEditableContent = { itemControl: "edit", id: GUID, @@ -82,22 +103,11 @@ describe("content editing:", () => { const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; expect(modelToUpdate.item.description).toBe(content.description); expect(modelToUpdate.item.properties.boundary).toBe("none"); + // No service is associated with Hub Initiatives + expect(getServiceSpy).not.toHaveBeenCalled(); + expect(updateServiceSpy).not.toHaveBeenCalled(); }); - }); - describe("update content with location:", () => { - it("converts to a model and updates the item", async () => { - const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( - Promise.resolve({ - item: { - typeKeywords: [], - }, - }) - ); - const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( - (m: IModel) => { - return Promise.resolve(m); - } - ); + it("handles when a location is explicitly set", async () => { const content: IHubEditableContent = { itemControl: "edit", id: GUID, @@ -129,6 +139,96 @@ describe("content editing:", () => { const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; expect(modelToUpdate.item.description).toBe(content.description); expect(modelToUpdate.item.properties.boundary).toBe("item"); + // No service is associated with Hub Initiatives + expect(getServiceSpy).not.toHaveBeenCalled(); + expect(updateServiceSpy).not.toHaveBeenCalled(); + }); + it("doesn't update the hosted service if configurations haven't changed", async () => { + const currentDefinition: Partial = + { capabilities: "Extract" }; + getServiceSpy.and.returnValue(Promise.resolve(currentDefinition)); + + const content: IHubEditableContent = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + location: { type: "item" }, + licenseInfo: "", + url: "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer", + // Indicates that Extract should enabled on the service, + // Since it already is, nothing should change + serverExtractCapability: true, + }; + const chk = await updateContent(content, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + expect(getItemSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + expect(getServiceSpy).toHaveBeenCalledTimes(1); + expect(updateServiceSpy).not.toHaveBeenCalled(); + }); + it("updates the hosted service if configurations have changed", async () => { + const currentDefinition: Partial = + { capabilities: "Query" }; + getServiceSpy.and.returnValue(Promise.resolve(currentDefinition)); + + const content: IHubEditableContent = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + location: { type: "item" }, + licenseInfo: "", + url: "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer", + // Indicates that Extract should enabled on the service, + // Since it currently isn't, the service will be updated + serverExtractCapability: true, + }; + const chk = await updateContent(content, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + expect(getItemSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + expect(getServiceSpy).toHaveBeenCalledTimes(1); + expect(updateServiceSpy).toHaveBeenCalledTimes(1); + const [url, { updateDefinition }] = updateServiceSpy.calls.argsFor(0); + expect(url).toEqual( + "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer" + ); + expect(updateDefinition).toEqual({ capabilities: "Query,Extract" }); }); }); describe("delete content", () => { diff --git a/packages/common/test/content/fetch.test.ts b/packages/common/test/content/fetch.test.ts index 3bee8066b7a..d527ecd720b 100644 --- a/packages/common/test/content/fetch.test.ts +++ b/packages/common/test/content/fetch.test.ts @@ -13,6 +13,17 @@ import * as _enrichmentsModule from "../../src/items/_enrichments"; import * as _fetchModule from "../../src/content/_fetch"; import * as documentItem from "../mocks/items/document.json"; import * as multiLayerFeatureServiceItem from "../mocks/items/multi-layer-feature-service.json"; +import { + HOSTED_FEATURE_SERVICE_DEFINITION, + HOSTED_FEATURE_SERVICE_GUID, + HOSTED_FEATURE_SERVICE_ITEM, + HOSTED_FEATURE_SERVICE_URL, + NON_HOSTED_FEATURE_SERVICE_GUID, + NON_HOSTED_FEATURE_SERVICE_ITEM, + PDF_GUID, + PDF_ITEM, +} from "./fixtures"; +import { MOCK_AUTH } from "../mocks/mock-auth"; // mock the item enrichments that would be returned for a multi-layer service const getMultiLayerItemEnrichments = () => { @@ -656,20 +667,70 @@ describe("fetchContent", () => { }); describe("fetchHubContent", () => { - it("defers to fetchContent", async () => { - const spy = spyOn( - require("../../src/content/fetch"), - "fetchContent" - ).and.returnValue( - Promise.resolve({ - item: { - type: "Feature Service", - id: "9001", - }, - }) + it("gets hosted feature service", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(HOSTED_FEATURE_SERVICE_ITEM) + ); + const getServiceSpy = spyOn( + featureLayerModule, + "getService" + ).and.returnValue(HOSTED_FEATURE_SERVICE_DEFINITION); + + const chk = await fetchHubContent(HOSTED_FEATURE_SERVICE_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(HOSTED_FEATURE_SERVICE_GUID); + expect(chk.owner).toBe(HOSTED_FEATURE_SERVICE_ITEM.owner); + expect(chk.serverExtractCapability).toBeTruthy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(HOSTED_FEATURE_SERVICE_GUID); + expect(getServiceSpy.calls.count()).toBe(1); + expect(getServiceSpy.calls.argsFor(0)[0].url).toBe( + HOSTED_FEATURE_SERVICE_URL ); - const fakeRequestOptions = {}; - await fetchHubContent("123", fakeRequestOptions); - expect(spy).toHaveBeenCalled(); + }); + + it("gets non-hosted feature service", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(NON_HOSTED_FEATURE_SERVICE_ITEM) + ); + const getServiceSpy = spyOn(featureLayerModule, "getService"); + + const chk = await fetchHubContent(NON_HOSTED_FEATURE_SERVICE_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(NON_HOSTED_FEATURE_SERVICE_GUID); + expect(chk.owner).toBe(NON_HOSTED_FEATURE_SERVICE_ITEM.owner); + expect(chk.serverExtractCapability).toBeFalsy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe( + NON_HOSTED_FEATURE_SERVICE_GUID + ); + // Service definition isn't fetched for non-hosted feature services + expect(getServiceSpy.calls.count()).toBe(0); + }); + + it("gets non-service content", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(PDF_ITEM) + ); + const getServiceSpy = spyOn(featureLayerModule, "getService"); + + const chk = await fetchHubContent(PDF_GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(PDF_GUID); + expect(chk.owner).toBe(PDF_ITEM.owner); + expect(chk.serverExtractCapability).toBeFalsy(); + expect(chk.hostedDownloads).toBeFalsy(); + + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(PDF_GUID); + // Service definition isn't fetched items that aren't hosted feature services + expect(getServiceSpy.calls.count()).toBe(0); }); }); diff --git a/packages/common/test/content/fixtures.ts b/packages/common/test/content/fixtures.ts new file mode 100644 index 00000000000..57d61cb82ea --- /dev/null +++ b/packages/common/test/content/fixtures.ts @@ -0,0 +1,65 @@ +import { IItem } from "@esri/arcgis-rest-portal"; +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; + +export const HOSTED_FEATURE_SERVICE_GUID = "9001"; +export const HOSTED_FEATURE_SERVICE_URL = + "https://services.arcgis.com/:orgId/arcgis/rest/services/:serviceName/FeatureServer"; +export const HOSTED_FEATURE_SERVICE_ITEM: IItem = { + id: HOSTED_FEATURE_SERVICE_GUID, + access: "public", + owner: "dev_pre_hub_admin", + created: 1652819949000, + modified: 1652819949000, + description: "This is a mock description", + snippet: "this is a content snippet", + isOrgItem: true, + title: "Mock Content", + type: "Feature Service", + typeKeywords: ["Hosted Service"], + tags: [], + categories: ["Basemap imagery", "Creative maps"], + thumbnail: "thumbnail/mock-thumbnail.png", + extent: [], + licenseInfo: "CC-BY-SA", + culture: "en-us", + contentOrigin: "self", + numViews: 10, + properties: {}, + size: 0, + url: HOSTED_FEATURE_SERVICE_URL, +}; +export const HOSTED_FEATURE_SERVICE_DEFINITION = { + capabilities: "Extract,Query", +} as IFeatureServiceDefinition; + +export const NON_HOSTED_FEATURE_SERVICE_GUID = "9002"; +export const NON_HOSTED_FEATURE_SERVICE_ITEM: IItem = { + ...HOSTED_FEATURE_SERVICE_ITEM, + id: NON_HOSTED_FEATURE_SERVICE_GUID, + typeKeywords: [], +}; + +export const PDF_GUID = "9003"; +export const PDF_ITEM: IItem = { + id: PDF_GUID, + access: "public", + owner: "dev_pre_hub_admin", + created: 1652819949000, + modified: 1652819949000, + description: "This is a mock description", + snippet: "this is a content snippet", + isOrgItem: true, + title: "Mock Content", + type: "PDF", + typeKeywords: [], + tags: [], + categories: [], + thumbnail: "thumbnail/mock-thumbnail.png", + extent: [], + licenseInfo: "CC-BY-SA", + culture: "en-us", + contentOrigin: "self", + numViews: 10, + properties: {}, + size: 1001, +}; diff --git a/packages/common/test/content/hostedServiceUtils.test.ts b/packages/common/test/content/hostedServiceUtils.test.ts new file mode 100644 index 00000000000..ee6b359ac8e --- /dev/null +++ b/packages/common/test/content/hostedServiceUtils.test.ts @@ -0,0 +1,86 @@ +import { IItem } from "@esri/arcgis-rest-portal"; +import { IFeatureServiceDefinition } from "@esri/arcgis-rest-types"; +import { IHubEditableContent } from "../../src"; +import { + hasServiceCapability, + isHostedFeatureServiceEntity, + isHostedFeatureServiceItem, + ServiceCapabilities, + toggleServiceCapability, +} from "../../src/content/hostedServiceUtils"; + +describe("isHostedFeatureServiceItem", () => { + it("returns true for hosted feature service items", () => { + const item = { + type: "Feature Service", + typeKeywords: ["Hosted Service"], + } as IItem; + + expect(isHostedFeatureServiceItem(item)).toBeTruthy(); + }); + + it("returns false for other items", () => { + const item = { type: "PDF" } as IItem; + expect(isHostedFeatureServiceItem(item)).toBeFalsy(); + }); +}); + +describe("isHostedFeatureServiceEntity", () => { + it("returns true for hosted feature service content entities", () => { + const entity = { + type: "Feature Service", + typeKeywords: ["Hosted Service"], + } as IHubEditableContent; + + expect(isHostedFeatureServiceEntity(entity)).toBeTruthy(); + }); + + it("returns false for other content entities", () => { + const entity = { type: "PDF" } as IHubEditableContent; + expect(isHostedFeatureServiceEntity(entity)).toBeFalsy(); + }); +}); + +describe("hasServiceCapability", () => { + it("returns false when no capabilities are defined", () => { + const result = hasServiceCapability( + ServiceCapabilities.EXTRACT, + {} as Partial + ); + expect(result).toBeFalsy(); + }); + it("returns false when capability is not included in the list", () => { + const result = hasServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query", + } as Partial); + expect(result).toBeFalsy(); + }); + it("returns true when capability is included in the list", () => { + const result = hasServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query,Extract", + } as Partial); + expect(result).toBeTruthy(); + }); +}); + +describe("toggleServiceCapability", () => { + it("turns capability on if none are defined", () => { + const result = toggleServiceCapability( + ServiceCapabilities.EXTRACT, + {} as Partial + ); + expect(result.capabilities).toBe("Extract"); + }); + it("turns capability on if not present", () => { + const result = toggleServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query", + } as Partial); + expect(result.capabilities).toBe("Query,Extract"); + }); + it("turns capability off if present", () => { + const result = toggleServiceCapability(ServiceCapabilities.EXTRACT, { + capabilities: "Query,Extract", + } as Partial); + expect(result.capabilities).toBe("Query"); + }); +}); diff --git a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts index f9f19e02c50..35c8fcb103d 100644 --- a/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts +++ b/packages/common/test/core/schemas/internal/getEntityEditorSchemas.test.ts @@ -19,6 +19,7 @@ import * as DiscussionBuildCreateUiSchema from "../../../../src/discussions/_int import { ContentEditorTypes } from "../../../../src/content/_internal/ContentSchema"; import * as ContentBuildEditUiSchema from "../../../../src/content/_internal/ContentUiSchemaEdit"; +import * as ContentBuildSettingsUiSchema from "../../../../src/content/_internal/ContentUiSchemaSettings"; import { PageEditorTypes } from "../../../../src/pages/_internal/PageSchema"; import * as PageBuildEditUiSchema from "../../../../src/pages/_internal/PageUiSchemaEdit"; @@ -42,6 +43,7 @@ describe("getEntityEditorSchemas: ", () => { { type: DiscussionEditorTypes[0], buildFn: DiscussionBuildEditUiSchema }, { type: DiscussionEditorTypes[1], buildFn: DiscussionBuildCreateUiSchema }, { type: ContentEditorTypes[0], buildFn: ContentBuildEditUiSchema }, + { type: ContentEditorTypes[1], buildFn: ContentBuildSettingsUiSchema }, { type: PageEditorTypes[0], buildFn: PageBuildEditUiSchema }, { type: GroupEditorTypes[0], buildFn: GroupBuildEditUiSchema }, { type: GroupEditorTypes[1], buildFn: GroupBuildSettingsUiSchema }, From 78243270291663caf22ed7031527c0fdc7e41414 Mon Sep 17 00:00:00 2001 From: Caleb Pomar Date: Wed, 13 Sep 2023 15:54:23 -0600 Subject: [PATCH 12/12] docs(hub-common): add missing js docs affects: @esri/hub-common --- .../common/src/content/hostedServiceUtils.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/common/src/content/hostedServiceUtils.ts b/packages/common/src/content/hostedServiceUtils.ts index 028c66d7c45..e2796700e7b 100644 --- a/packages/common/src/content/hostedServiceUtils.ts +++ b/packages/common/src/content/hostedServiceUtils.ts @@ -2,16 +2,33 @@ import { IFeatureServiceDefinition } from "@esri/arcgis-rest-feature-layer"; import { IItem } from "@esri/arcgis-rest-portal"; import { IHubEditableContent } from "../core/types/IHubEditableContent"; +/** + * Determines whether an item represents a hosted feature service + * @param item item to check + * @returns whether the item represents a hosted feature service + */ export function isHostedFeatureServiceItem(item: IItem): boolean { return isHostedFeatureService(item.type, item.typeKeywords); } +/** + * Determines whether an entity represents a hosted feature service + * @param item item to check + * @returns whether the item represents a hosted feature service + */ export function isHostedFeatureServiceEntity( content: IHubEditableContent ): boolean { return isHostedFeatureService(content.type, content.typeKeywords); } +/** + * @private + * base helper to determine whether the arguments correspond to a hosted feature service + * @param type an item type + * @param typeKeywords an item typeKeywords array + * @returns whether the arguments correspond to a hosted feature service + */ function isHostedFeatureService( type: string, typeKeywords: string[] = [] @@ -61,6 +78,13 @@ export function toggleServiceCapability( return updatedDefinition; } +/** + * @private + * adds a capability onto a service definition + * @param capability capability to add + * @param serviceDefinition the definition to modify + * @returns a copy of the modified definition + */ function addServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial @@ -74,6 +98,13 @@ function addServiceCapability( return updated; } +/** + * @private + * removes a capability from a service definition + * @param capability capability to remove + * @param serviceDefinition the definition to modify + * @returns a copy of the modified definition + */ function removeServiceCapability( capability: ServiceCapabilities, serviceDefinition: Partial