diff --git a/package-lock.json b/package-lock.json index c947eb76b01..85c97060974 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22688,9 +22688,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", @@ -22705,21 +22706,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" } @@ -22733,9 +22737,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", @@ -22753,9 +22758,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", @@ -65010,7 +65016,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "14.98.1", + "version": "14.99.0", "license": "Apache-2.0", "dependencies": { "@terraformer/arcgis": "^2.1.2", @@ -83465,7 +83471,8 @@ "lodash._baseindexof": { "version": "3.1.0", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -83480,17 +83487,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" } @@ -83504,7 +83514,8 @@ "lodash._getnative": { "version": "3.9.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._root": { "version": "3.0.1", @@ -83521,7 +83532,8 @@ "lodash.restparam": { "version": "3.6.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash.union": { "version": "4.6.0", diff --git a/packages/common/src/search/_internal/commonHelpers/getApi.ts b/packages/common/src/search/_internal/commonHelpers/getApi.ts index fa81d64c37a..a1c2db4c329 100644 --- a/packages/common/src/search/_internal/commonHelpers/getApi.ts +++ b/packages/common/src/search/_internal/commonHelpers/getApi.ts @@ -33,7 +33,7 @@ export function getApi( } else if (shouldUseDiscussionsApi(targetEntity, options)) { result = getDiscussionsApiDefinition(); } else if (shouldUseOgcApi(targetEntity, options)) { - result = getOgcApiDefinition(options); + result = getOgcApiDefinition(targetEntity, options); } else { result = { type: "arcgis", url: portal }; } diff --git a/packages/common/src/search/_internal/commonHelpers/getOgcApiDefinition.ts b/packages/common/src/search/_internal/commonHelpers/getOgcApiDefinition.ts index 60c3fbf86bf..8544bcf0e0a 100644 --- a/packages/common/src/search/_internal/commonHelpers/getOgcApiDefinition.ts +++ b/packages/common/src/search/_internal/commonHelpers/getOgcApiDefinition.ts @@ -1,3 +1,4 @@ +import { EntityType } from "../../types"; import { IHubSearchOptions } from "../../types/IHubSearchOptions"; import { IApiDefinition } from "../../types/types"; @@ -9,11 +10,17 @@ import { IApiDefinition } from "../../types/types"; * @returns an IApiDefinition with needed info to target the OGC API */ export function getOgcApiDefinition( + targetEntity: EntityType, options: IHubSearchOptions ): IApiDefinition { const umbrellaDomain = new URL(options.requestOptions.hubApiUrl).hostname; - return { - type: "arcgis-hub", - url: `https://${umbrellaDomain}/api/search/v1`, - }; + return targetEntity === "discussionPost" + ? { + type: "arcgis-hub", + url: `https://${umbrellaDomain}/api/search/v2`, + } + : { + type: "arcgis-hub", + url: `https://${umbrellaDomain}/api/search/v1`, + }; } diff --git a/packages/common/src/search/_internal/commonHelpers/shouldUseOgcApi.ts b/packages/common/src/search/_internal/commonHelpers/shouldUseOgcApi.ts index 8d6dc06641b..560bfd6d90e 100644 --- a/packages/common/src/search/_internal/commonHelpers/shouldUseOgcApi.ts +++ b/packages/common/src/search/_internal/commonHelpers/shouldUseOgcApi.ts @@ -15,5 +15,7 @@ export function shouldUseOgcApi( site, requestOptions: { isPortal }, } = options; - return targetEntity === "item" && !!site && !isPortal; + if (isPortal) return false; + if (targetEntity === "discussionPost") return true; + return targetEntity === "item" && !!site; } diff --git a/packages/common/src/search/_internal/hubSearchItemsHelpers/formatOgcItemsResponse.ts b/packages/common/src/search/_internal/hubSearchItemsHelpers/formatOgcItemsResponse.ts index 78de62d7fca..a4e4cc6b167 100644 --- a/packages/common/src/search/_internal/hubSearchItemsHelpers/formatOgcItemsResponse.ts +++ b/packages/common/src/search/_internal/hubSearchItemsHelpers/formatOgcItemsResponse.ts @@ -1,15 +1,55 @@ import { IQuery } from "../../types/IHubCatalog"; import { IHubSearchOptions } from "../../types/IHubSearchOptions"; import { IHubSearchResponse } from "../../types/IHubSearchResponse"; -import { IHubSearchResult } from "../../types/IHubSearchResult"; import { getNextOgcCallback } from "./getNextOgcCallback"; import { IOgcItemsResponse } from "./interfaces"; import { ogcItemToSearchResult } from "./ogcItemToSearchResult"; +import { ogcItemToDiscussionPostResult } from "./ogcItemToDiscussionPostResult"; +import { IHubSearchResult } from "../../types"; export async function formatOgcItemsResponse( response: IOgcItemsResponse, originalQuery: IQuery, originalOptions: IHubSearchOptions +): Promise> { + if (originalQuery.targetEntity === "discussionPost") { + return formatDiscussionPostTargetEntityResponse( + response, + originalQuery, + originalOptions + ); + } + + return formatItemTargetEntityResponse( + response, + originalQuery, + originalOptions + ); +} + +async function formatDiscussionPostTargetEntityResponse( + response: IOgcItemsResponse, + originalQuery: IQuery, + originalOptions: IHubSearchOptions +): Promise> { + const formattedResults = await Promise.all( + response.features.map((f) => ogcItemToDiscussionPostResult(f)) + ); + const next = getNextOgcCallback(response, originalQuery, originalOptions); + const nextLink = response.links.find((l) => l.rel === "next"); + + return { + total: response.numberMatched, + results: formattedResults, + hasNext: !!nextLink, + next, + }; +} + +async function formatItemTargetEntityResponse( + response: IOgcItemsResponse, + originalQuery: IQuery, + originalOptions: IHubSearchOptions ): Promise> { const formattedResults = await Promise.all( response.features.map((f) => diff --git a/packages/common/src/search/_internal/hubSearchItemsHelpers/getOgcCollectionUrl.ts b/packages/common/src/search/_internal/hubSearchItemsHelpers/getOgcCollectionUrl.ts index 716958abd65..cb46dfabf8d 100644 --- a/packages/common/src/search/_internal/hubSearchItemsHelpers/getOgcCollectionUrl.ts +++ b/packages/common/src/search/_internal/hubSearchItemsHelpers/getOgcCollectionUrl.ts @@ -15,6 +15,11 @@ import { IApiDefinition } from "../../types/types"; */ export function getOgcCollectionUrl(query: IQuery, options: IHubSearchOptions) { const apiDefinition = options.api as IApiDefinition; + // Discussion posts as a target entity will be searchable with one collection, + // so simply use that for the URL + if (query.targetEntity === "discussionPost") { + return `${apiDefinition.url}/collections/discussion-post`; + } const collectionId = query.collection || "all"; return `${apiDefinition.url}/collections/${collectionId}`; } diff --git a/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToDiscussionPostResult.ts b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToDiscussionPostResult.ts new file mode 100644 index 00000000000..fa71597f138 --- /dev/null +++ b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToDiscussionPostResult.ts @@ -0,0 +1,34 @@ +import { IOgcItem } from "./interfaces"; +import { IPost } from "../../../discussions"; +import { IHubSearchResult } from "../../types"; + +/** + * This method is responsible for converting an OGC item whose properties + * represent an IPost into an IHubSearchResult. Although some fields do not + * apply, this is being done such that result of a discussion post search + * can automatically be used in a gallery. + * @param ogcItem + * @returns IHubSearchResult + */ +export async function ogcItemToDiscussionPostResult( + ogcItem: IOgcItem +): Promise { + return { + id: ogcItem.id, + name: ogcItem.properties.title, + summary: ogcItem.properties.body, + createdDate: new Date(ogcItem.properties.createdAt), + createdDateSource: "properties.createdAt", + updatedDate: new Date(ogcItem.properties.updatedAt), + updatedDateSource: "properties.updatedAt", + type: ogcItem.properties.postType, + owner: ogcItem.properties.creator, + location: ogcItem.properties.geometry, + created: new Date(ogcItem.properties.createdAt), + modified: new Date(ogcItem.properties.updatedAt), + title: ogcItem.properties.title, + rawResult: ogcItem.properties, + access: null, + family: null, + } as IHubSearchResult; +} diff --git a/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts index cbd9b47da46..67f365d1064 100644 --- a/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts +++ b/packages/common/src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult.ts @@ -1,9 +1,5 @@ import { IItem } from "@esri/arcgis-rest-types"; -import { - IHubGeography, - IHubRequestOptions, - IPolygonProperties, -} from "../../../types"; +import { IHubRequestOptions } from "../../../types"; import { IHubSearchResult } from "../../types/IHubSearchResult"; import { itemToSearchResult } from "../portalSearchItems"; import { IOgcItem } from "./interfaces"; diff --git a/packages/common/src/search/hubSearch.ts b/packages/common/src/search/hubSearch.ts index 38826ad0c78..36e163d627d 100644 --- a/packages/common/src/search/hubSearch.ts +++ b/packages/common/src/search/hubSearch.ts @@ -85,6 +85,7 @@ export async function hubSearch( "arcgis-hub": { item: hubSearchItems, channel: hubSearchChannels, + discussionPost: hubSearchItems, }, }; diff --git a/packages/common/src/search/types/IHubCatalog.ts b/packages/common/src/search/types/IHubCatalog.ts index e8292451acf..4497581b408 100644 --- a/packages/common/src/search/types/IHubCatalog.ts +++ b/packages/common/src/search/types/IHubCatalog.ts @@ -61,7 +61,8 @@ export type EntityType = | "communityUser" | "groupMember" | "event" - | "channel"; + | "channel" + | "discussionPost"; /** * @private * diff --git a/packages/common/test/search/_internal/getApi.test.ts b/packages/common/test/search/_internal/getApi.test.ts index 41b35c7a66e..fe561554f80 100644 --- a/packages/common/test/search/_internal/getApi.test.ts +++ b/packages/common/test/search/_internal/getApi.test.ts @@ -57,4 +57,17 @@ describe("getApi", () => { url: portal, }); }); + it("otherwise returns reference to OGC API V2 API if targetEntity is discussionPost", () => { + const options = { + site, + requestOptions: { + hubApiUrl, + isPortal: false, + }, + } as unknown as IHubSearchOptions; + expect(getApi("discussionPost", options)).toEqual({ + type: "arcgis-hub", + url: `${hubApiUrl}/api/search/v2`, + }); + }); }); diff --git a/packages/common/test/search/_internal/hubSearchItems.test.ts b/packages/common/test/search/_internal/hubSearchItems.test.ts index c90eec2d455..24608ad3828 100644 --- a/packages/common/test/search/_internal/hubSearchItems.test.ts +++ b/packages/common/test/search/_internal/hubSearchItems.test.ts @@ -6,6 +6,7 @@ import { IHubSearchOptions, IHubSearchResponse, IHubSearchResult, + IPost, IPredicate, IQuery, } from "../../../src"; @@ -25,12 +26,14 @@ import { getOgcAggregationQueryParams } from "../../../src/search/_internal/hubS import { getQQueryParam } from "../../../src/search/_internal/hubSearchItemsHelpers/getQQueryParam"; import { IOgcItem } from "../../../src/search/_internal/hubSearchItemsHelpers/interfaces"; import * as ogcItemToSearchResultModule from "../../../src/search/_internal/hubSearchItemsHelpers/ogcItemToSearchResult"; +import * as ogcItemToDiscussionPostModule from "../../../src/search/_internal/hubSearchItemsHelpers/ogcItemToDiscussionPostResult"; import { formatOgcItemsResponse } from "../../../src/search/_internal/hubSearchItemsHelpers/formatOgcItemsResponse"; import { formatOgcAggregationsResponse } from "../../../src/search/_internal/hubSearchItemsHelpers/formatOgcAggregationsResponse"; import * as searchOgcItemsModule from "../../../src/search/_internal/hubSearchItemsHelpers/searchOgcItems"; import * as portalSearchItemsModule from "../../../src/search/_internal/portalSearchItems"; import * as fetchMock from "fetch-mock"; import { + ogcDiscussionPostResponseWithNext, ogcItemsResponse, ogcItemsResponseWithNext, } from "./mocks/ogcItemsResponse"; @@ -75,6 +78,22 @@ describe("hubSearchItems Module |", () => { "https://my-hub.com/api/search/v1/collections/dataset" ); }); + it("points to the V2 Discussion post collection if the targetEntity is discussionPost", () => { + const query: IQuery = { + targetEntity: "discussionPost", + filters: [], + }; + const options: IHubSearchOptions = { + api: { + type: "arcgis-hub", + url: "https://my-hub.com/api/search/v2", + }, + }; + const result = getOgcCollectionUrl(query, options); + expect(result).toBe( + "https://my-hub.com/api/search/v2/collections/discussion-post" + ); + }); }); describe("formatPredicate |", () => { @@ -830,6 +849,59 @@ describe("hubSearchItems Module |", () => { }); }); + describe("ogcItemToDiscussionPostResult |", () => { + const { ogcItemToDiscussionPostResult } = ogcItemToDiscussionPostModule; + + const ogcItemProperties: IPost = { + id: "12345", + title: "title", + body: "body", + creator: "creator", + status: "PENDING" as any, + appInfo: null, + discussion: null, + geometry: null, + featureGeometry: null, + postType: "Discussion" as any, + createdAt: new Date("2021-01-01"), + updatedAt: new Date("2021-01-01"), + }; + + it("returns an IHubSearchResult with an IPost raw result", async () => { + const ogcItem: IOgcItem = { + id: "9001", + type: "Feature", + geometry: null, // for simplicity + time: null, // for simplicity + links: [], // for simplicity + properties: cloneObject(ogcItemProperties), + }; + const includes: string[] = []; + const requestOptions: IHubRequestOptions = {}; + + const result = await ogcItemToDiscussionPostResult(ogcItem); + + expect(result).toEqual({ + access: null as any, + name: "title", + title: "title", + type: "Discussion", + createdDate: ogcItemProperties.createdAt, + createdDateSource: "properties.createdAt", + updatedDate: ogcItemProperties.updatedAt, + updatedDateSource: "properties.updatedAt", + created: ogcItemProperties.createdAt, + modified: ogcItemProperties.updatedAt, + family: null as any, + id: "9001", + owner: "creator", + rawResult: ogcItemProperties as any, + summary: "body", + location: null as any, + } as any); + }); + }); + describe("getNextOgcCallback", () => { const { getNextOgcCallback } = getNextOgcCallbackModule; const query: IQuery = { @@ -915,6 +987,18 @@ describe("hubSearchItems Module |", () => { expect(formattedResponse.total).toEqual(2); expect(formattedResponse.hasNext).toEqual(true); // Verify that hasNext is true this time }); + + it("correctly handles discussion posts", async () => { + const formattedResponse = await formatOgcItemsResponse( + ogcDiscussionPostResponseWithNext, + { targetEntity: "discussionPost", filters: [] }, + requestOptions + ); + + expect(formattedResponse).toBeDefined(); + expect(formattedResponse.total).toEqual(2); + expect(formattedResponse.hasNext).toEqual(true); // Verify that hasNext is true this time + }); }); describe("formatOgcAggregationsResponse |", () => { diff --git a/packages/common/test/search/_internal/mocks/ogcItemsResponse.ts b/packages/common/test/search/_internal/mocks/ogcItemsResponse.ts index 5f23f7acadf..b7791da7625 100644 --- a/packages/common/test/search/_internal/mocks/ogcItemsResponse.ts +++ b/packages/common/test/search/_internal/mocks/ogcItemsResponse.ts @@ -179,3 +179,76 @@ export const ogcItemsResponseWithNext: IOgcItemsResponse = { }, ], }; + +export const ogcDiscussionPostResponseWithNext: IOgcItemsResponse = { + type: "FeatureCollection", + features: [ + { + id: "f4bcc", + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [-121.11799999999793, 39.37030746927015], + [-119.00899999999801, 39.37030746927015], + [-119.00899999999801, 38.67499450446548], + [-121.11799999999793, 38.67499450446548], + [-121.11799999999793, 39.37030746927015], + ], + ], + }, + properties: { + id: "12345", + title: "title", + body: "body", + status: "PENDING" as any, + appInfo: null, + discussion: null, + geometry: null, + featureGeometry: null, + postType: "Discussion" as any, + createdAt: new Date("2021-01-01"), + updatedAt: new Date("2021-01-01"), + }, + time: null, + links: [ + { + rel: "self", + type: "application/geo+json", + title: "This document as GeoJSON", + href: "https://foo-bar.com/api/search/v2/collections/discussion-post/items/12345", + }, + { + rel: "collection", + type: "application/json", + title: "discussion-post", + href: "https://foo-bar.com/api/search/v2/collections/discussion-post", + }, + ], + }, + ], + timestamp: "2023-01-23T18:53:40.715Z", + numberMatched: 2, + numberReturned: 1, + links: [ + { + rel: "self", + type: "application/geo+json", + title: "This document as GeoJSON", + href: "https://foo-bar.com/api/search/v1/collections/all/items", + }, + { + rel: "collection", + type: "application/json", + title: "All", + href: "https://foo-bar.com/api/search/v1/collections/all", + }, + { + rel: "next", + type: "application/geo+json", + title: "items (next)", + href: "https://foo-bar.com/api/search/v1/collections/all/items?limit=1&startindex=2", + }, + ], +}; diff --git a/packages/common/test/search/_internal/shouldUseOgcApi.test.ts b/packages/common/test/search/_internal/shouldUseOgcApi.test.ts index 33fd9e092e1..53940ad4668 100644 --- a/packages/common/test/search/_internal/shouldUseOgcApi.test.ts +++ b/packages/common/test/search/_internal/shouldUseOgcApi.test.ts @@ -35,6 +35,16 @@ describe("shouldUseOgcApi", () => { expect(shouldUseOgcApi(targetEntity, options)).toBeFalsy(); }); + it("returns true when target entity is 'discussionPost'", () => { + const targetEntity = "discussionPost"; + const options = { + requestOptions: { + isPortal: false, + }, + } as unknown as IHubSearchOptions; + expect(shouldUseOgcApi(targetEntity, options)).toBeTruthy(); + }); + it("returns true otherwise", () => { const targetEntity = "item"; const options = { diff --git a/packages/common/test/search/hubSearch.test.ts b/packages/common/test/search/hubSearch.test.ts index 8fcf6c7d308..2ea0459b11f 100644 --- a/packages/common/test/search/hubSearch.test.ts +++ b/packages/common/test/search/hubSearch.test.ts @@ -280,6 +280,40 @@ describe("hubSearch Module:", () => { expect(query).toEqual(qry); expect(options).toEqual(opts); }); + it("discussionPost + arcgis-hub: hubSearchItems", async () => { + const qry: IQuery = { + targetEntity: "discussionPost", + collection: "discussion-post" as any, + filters: [ + { + predicates: [{ term: "water" }], + }, + ], + }; + const opts: IHubSearchOptions = { + site: "https://my-site.hub.arcgis.com", + requestOptions: { + isPortal: false, + portal: "https://qaext.arcgis.com/sharing/rest", + hubApiUrl: "https://hubqa.arcgis.com", + }, + include: ["server"], + }; + const chk = await hubSearch(qry, opts); + expect(chk.total).toBe(99); + expect(portalSearchItemsSpy.calls.count()).toBe(0); + expect(portalSearchGroupsSpy.calls.count()).toBe(0); + expect(hubSearchItemsSpy.calls.count()).toBe(1); + const [query, options] = hubSearchItemsSpy.calls.argsFor(0); + expect(query).toEqual(qry); + expect(options.include).toBeDefined(); + // Any cloning of auth can break downstream functions + expect(options.requestOptions).toBe(opts.requestOptions); + expect(options.api).toEqual({ + type: "arcgis-hub", + url: "https://hubqa.arcgis.com/api/search/v2", + }); + }); }); }); });