diff --git a/.changeset/cold-teams-sit.md b/.changeset/cold-teams-sit.md new file mode 100644 index 000000000..75586c730 --- /dev/null +++ b/.changeset/cold-teams-sit.md @@ -0,0 +1,45 @@ +--- +"@esri/arcgis-rest-basemap-sessions": major +"@esri/arcgis-rest-developer-credentials": major +"@esri/arcgis-rest-elevation": major +"@esri/arcgis-rest-feature-service": major +"@esri/arcgis-rest-geocoding": major +"@esri/arcgis-rest-places": major +"@esri/arcgis-rest-portal": major +"@esri/arcgis-rest-request": major +--- + +remove support for rawResponse and update packages to new IRequestOptions shape + +### Summary + +This change drops support for `rawResponse` from all packages, updates all packages to use the new `IRequestOptions` shape, and adds support for raw requests with new methods. + +- `rawResponse` support removed from REST JS +- fetch methods now return JSON only. Non-JSON data can be fetched using `rawRequest()` or new wrapper methods that implement it. +- added `queryFeaturesRaw()` and `getItemDataRaw()` +- converted all `httpMethod` instances to the new `fetchOptions.method` + +### Why these changes were made + +- REST JS methods are being moved to return JSON only in most cases. +- Generic types will be easier to implement and improve our TypeScript commitment. +- Improve API surface area and our testing landscape. +- Separate default function from more advanced uses. + +### Breaking Changes + +- `getFeature()` no longer supports raw response. +- `queryFeatures()` can no longer be used to query features as pbf with `f=pbf`. +- `bulkGeocode()` no longer supports raw response. +- `geocode()` no longer supports raw response. +- `getItemData()` no longer supports raw, file, or binary responses, use getItemDataRaw()instead. + deprecated IItemDataOptions since we don't support the file property in getItemData() anymore. Use getItemDataRaw() to get the native response instead. +- `getItemInfo()` no longer supports rawResponse as an option and uses rawRequest() as default behavior. +- `getItemResource()` no longer supports rawResponse as an option and uses rawRequest() as default behavior. + +### Migration guide + +- To get a raw response, construct your own request and use `rawRequest()` to fetch a native response. +- For querying features as `pbf`, use `queryFeaturesRaw()` and supply `f=pbf` in the query options. +- To manually set an HTTP method, supply `fetchOptions.method` in IRequestOptions instead of top-level `httpMethod`. diff --git a/packages/arcgis-rest-basemap-sessions/src/utils/startNewSession.ts b/packages/arcgis-rest-basemap-sessions/src/utils/startNewSession.ts index c37ff43ad..e1adf0839 100644 --- a/packages/arcgis-rest-basemap-sessions/src/utils/startNewSession.ts +++ b/packages/arcgis-rest-basemap-sessions/src/utils/startNewSession.ts @@ -23,7 +23,7 @@ export function startNewSession({ duration = DEFAULT_DURATION }: IRequestNewSessionParams): Promise { return request(startSessionUrl, { - httpMethod: "GET", + fetchOptions: { method: "GET" }, authentication: authentication, params: { styleFamily, durationSeconds: duration } }); diff --git a/packages/arcgis-rest-developer-credentials/src/createApiKey.ts b/packages/arcgis-rest-developer-credentials/src/createApiKey.ts index 4f3c16281..2b1ba500d 100644 --- a/packages/arcgis-rest-developer-credentials/src/createApiKey.ts +++ b/packages/arcgis-rest-developer-credentials/src/createApiKey.ts @@ -62,7 +62,10 @@ import { getRegisteredAppInfo } from "./shared/getRegisteredAppInfo.js"; export async function createApiKey( requestOptions: ICreateApiKeyOptions ): Promise { - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; // filter param buckets: const baseRequestOptions = extractBaseRequestOptions(requestOptions); // snapshot of basic IRequestOptions before customized params being built into it diff --git a/packages/arcgis-rest-developer-credentials/src/createOAuthApp.ts b/packages/arcgis-rest-developer-credentials/src/createOAuthApp.ts index a53f35654..efff7b7ae 100644 --- a/packages/arcgis-rest-developer-credentials/src/createOAuthApp.ts +++ b/packages/arcgis-rest-developer-credentials/src/createOAuthApp.ts @@ -48,7 +48,10 @@ import { ICreateOAuthAppOption, IOAuthApp } from "./shared/types/oAuthType.js"; export async function createOAuthApp( requestOptions: ICreateOAuthAppOption ): Promise { - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; // filter param buckets: diff --git a/packages/arcgis-rest-developer-credentials/src/shared/getRegisteredAppInfo.ts b/packages/arcgis-rest-developer-credentials/src/shared/getRegisteredAppInfo.ts index 2f4ca60b4..688ded4bb 100644 --- a/packages/arcgis-rest-developer-credentials/src/shared/getRegisteredAppInfo.ts +++ b/packages/arcgis-rest-developer-credentials/src/shared/getRegisteredAppInfo.ts @@ -42,7 +42,10 @@ export async function getRegisteredAppInfo( const url = getPortalUrl(requestOptions) + `/content/users/${userName}/items/${requestOptions.itemId}/registeredAppInfo`; - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; const registeredAppResponse: IRegisteredAppResponse = await request(url, { ...requestOptions, diff --git a/packages/arcgis-rest-developer-credentials/src/shared/helpers.ts b/packages/arcgis-rest-developer-credentials/src/shared/helpers.ts index 48bb313d5..a22937f53 100644 --- a/packages/arcgis-rest-developer-credentials/src/shared/helpers.ts +++ b/packages/arcgis-rest-developer-credentials/src/shared/helpers.ts @@ -87,15 +87,9 @@ export function extractBaseRequestOptions( options: T ): Partial { const requestOptionsProperties: Array = [ - "credentials", - "headers", - "hideToken", - "httpMethod", - "maxUrlLength", "portal", - "rawResponse", - "signal", - "suppressWarnings" + "requestFlags", + "fetchOptions" ]; return filterKeys(options, requestOptionsProperties); diff --git a/packages/arcgis-rest-developer-credentials/src/shared/unregisterApp.ts b/packages/arcgis-rest-developer-credentials/src/shared/unregisterApp.ts index 45b89ce8d..fb4fc05db 100644 --- a/packages/arcgis-rest-developer-credentials/src/shared/unregisterApp.ts +++ b/packages/arcgis-rest-developer-credentials/src/shared/unregisterApp.ts @@ -36,7 +36,10 @@ import { request } from "@esri/arcgis-rest-request"; export async function unregisterApp( requestOptions: IUnregisterAppOptions ): Promise { - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; // get app const baseRequestOptions = extractBaseRequestOptions(requestOptions); diff --git a/packages/arcgis-rest-developer-credentials/src/updateApiKey.ts b/packages/arcgis-rest-developer-credentials/src/updateApiKey.ts index 55b364145..af1e4dc70 100644 --- a/packages/arcgis-rest-developer-credentials/src/updateApiKey.ts +++ b/packages/arcgis-rest-developer-credentials/src/updateApiKey.ts @@ -63,7 +63,10 @@ import { export async function updateApiKey( requestOptions: IUpdateApiKeyOptions ): Promise { - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; const baseRequestOptions = extractBaseRequestOptions(requestOptions); // get base requestOptions snapshot /** diff --git a/packages/arcgis-rest-developer-credentials/src/updateOAuthApp.ts b/packages/arcgis-rest-developer-credentials/src/updateOAuthApp.ts index a3778f843..5699d4791 100644 --- a/packages/arcgis-rest-developer-credentials/src/updateOAuthApp.ts +++ b/packages/arcgis-rest-developer-credentials/src/updateOAuthApp.ts @@ -50,7 +50,10 @@ import { getRegisteredAppInfo } from "./shared/getRegisteredAppInfo.js"; export async function updateOAuthApp( requestOptions: IUpdateOAuthOptions ): Promise { - requestOptions.httpMethod = "POST"; + requestOptions.fetchOptions = { + ...requestOptions.fetchOptions, + method: "POST" + }; // get app const baseRequestOptions = extractBaseRequestOptions(requestOptions); // get base requestOptions snapshot diff --git a/packages/arcgis-rest-developer-credentials/test/getApiKey.test.ts b/packages/arcgis-rest-developer-credentials/test/getApiKey.test.ts index ad69abf49..6f15d1432 100644 --- a/packages/arcgis-rest-developer-credentials/test/getApiKey.test.ts +++ b/packages/arcgis-rest-developer-credentials/test/getApiKey.test.ts @@ -211,7 +211,7 @@ describe("getApiKey()", () => { const apiKeyResponse = await getApiKey({ itemId: "cddcacee5848488bb981e6c6ff91ab79", authentication: authOnline, - httpMethod: "GET" + fetchOptions: { method: "GET" } }); // verify first fetch diff --git a/packages/arcgis-rest-developer-credentials/test/shared/getRegisteredAppInfo.test.ts b/packages/arcgis-rest-developer-credentials/test/shared/getRegisteredAppInfo.test.ts index 9f37c6b53..95748e2b1 100644 --- a/packages/arcgis-rest-developer-credentials/test/shared/getRegisteredAppInfo.test.ts +++ b/packages/arcgis-rest-developer-credentials/test/shared/getRegisteredAppInfo.test.ts @@ -151,7 +151,7 @@ describe("registerApp()", () => { const requestOptions: IGetAppInfoOptions = { itemId: "fake-itemID", authentication: authOnline, - httpMethod: "GET" + fetchOptions: { method: "GET" } }; const appResponse = await getRegisteredAppInfo(requestOptions); diff --git a/packages/arcgis-rest-developer-credentials/test/shared/registerApp.test.ts b/packages/arcgis-rest-developer-credentials/test/shared/registerApp.test.ts index c205b0990..0ff15825d 100644 --- a/packages/arcgis-rest-developer-credentials/test/shared/registerApp.test.ts +++ b/packages/arcgis-rest-developer-credentials/test/shared/registerApp.test.ts @@ -170,7 +170,7 @@ describe("registerApp()", () => { httpReferrers: ["https://www.esri.com/en-us/home"], privileges: [], authentication: authOnline, - httpMethod: "GET" + fetchOptions: { method: "GET" } }; const appResponse = await registerApp(requestOptions); diff --git a/packages/arcgis-rest-elevation/src/findElevationAtManyPoints.ts b/packages/arcgis-rest-elevation/src/findElevationAtManyPoints.ts index d1b0e9017..8aeec5fa6 100644 --- a/packages/arcgis-rest-elevation/src/findElevationAtManyPoints.ts +++ b/packages/arcgis-rest-elevation/src/findElevationAtManyPoints.ts @@ -23,11 +23,18 @@ type successResponse = */ export interface IFindElevationAtManyPointsResponse extends successResponse {} +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode findElevationAtPoint}. */ export interface IFindElevationAtManyPointsOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams {} /** diff --git a/packages/arcgis-rest-elevation/src/findElevationAtPoint.ts b/packages/arcgis-rest-elevation/src/findElevationAtPoint.ts index 0111cfbf4..6dd5f96a5 100644 --- a/packages/arcgis-rest-elevation/src/findElevationAtPoint.ts +++ b/packages/arcgis-rest-elevation/src/findElevationAtPoint.ts @@ -23,11 +23,18 @@ type successResponse = */ export interface IFindElevationAtPointResponse extends successResponse {} +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode findElevationAtPoint}. */ export interface IFindElevationAtPointOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams {} /** @@ -68,7 +75,10 @@ export function findElevationAtPoint( return ( request(`${baseUrl}/elevation/at-point`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } }) as Promise ).then((response) => { const r: IFindElevationAtPointResponse = { diff --git a/packages/arcgis-rest-feature-service/src/getAttachments.ts b/packages/arcgis-rest-feature-service/src/getAttachments.ts index ba7f7517e..8a21d036a 100644 --- a/packages/arcgis-rest-feature-service/src/getAttachments.ts +++ b/packages/arcgis-rest-feature-service/src/getAttachments.ts @@ -44,8 +44,11 @@ export function getAttachments( requestOptions: IGetAttachmentsOptions ): Promise<{ attachmentInfos: IAttachmentInfo[] }> { const options: IGetAttachmentsOptions = { - httpMethod: "GET", - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions.fetchOptions + } }; // pass through diff --git a/packages/arcgis-rest-feature-service/src/query.ts b/packages/arcgis-rest-feature-service/src/query.ts index 5b26e0f3b..2dc841cd7 100644 --- a/packages/arcgis-rest-feature-service/src/query.ts +++ b/packages/arcgis-rest-feature-service/src/query.ts @@ -80,7 +80,7 @@ export interface IQueryFeaturesOptions extends ISharedQueryOptions { /** * Response format. Defaults to "json". */ - f?: "json" | "geojson" | "pbf" | "pbf-as-geojson" | "pbf-as-arcgis"; + f?: "json" | "geojson" | "pbf-as-geojson" | "pbf-as-arcgis"; /** * someday... * @@ -133,7 +133,7 @@ export interface IQueryAllFeaturesOptions extends ISharedQueryOptions { returnExceededLimitFeatures?: true; /** * Response format. Defaults to "json" - * NOTE: for "pbf" you must use the method `rawRequest()` + * NOTE: for "f=pbf" you must use the method `queryFeaturesRaw()` * and parse the response yourself using `response.arrayBuffer()` */ f?: "json" | "geojson" | "pbf-as-geojson" | "pbf-as-arcgis"; @@ -156,6 +156,75 @@ export interface IQueryResponse { objectIds?: number[]; } +export interface IQueryFeaturesRawOptions + extends Omit { + /** + * Response format for raw queries. Includes "pbf" for callers that need direct binary handling. + */ + f?: IQueryFeaturesOptions["f"] | "pbf"; +} + +function prepareQueryFeaturesOptions( + requestOptions: IQueryFeaturesRawOptions | IQueryFeaturesOptions +): IRequestOptions { + const queryOptions = appendCustomParams( + requestOptions, + [ + "where", + "objectIds", + "relationParam", + "time", + "distance", + "units", + "outFields", + "geometry", + "geometryType", + "spatialRel", + "returnGeometry", + "maxAllowableOffset", + "geometryPrecision", + "inSR", + "outSR", + "gdbVersion", + "returnDistinctValues", + "returnIdsOnly", + "returnCountOnly", + "returnExtentOnly", + "orderByFields", + "groupByFieldsForStatistics", + "outStatistics", + "returnZ", + "returnM", + "multipatchOption", + "resultOffset", + "resultRecordCount", + "quantizationParameters", + "returnCentroid", + "resultType", + "historicMoment", + "returnTrueCurves", + "sqlFormat", + "returnExceededLimitFeatures", + "f" + ], + { + params: { + // set default query parameters + where: "1=1", + outFields: "*", + ...requestOptions.params + } + } + ); + + queryOptions.fetchOptions = { + method: "GET", + ...queryOptions.fetchOptions + }; + + return queryOptions; +} + /** * Query and decode pbf features on the client. Improves performance on slow networks and large queries. * Handles both f=pbf-as-geojson and f=pbf-as-arcgis format query params and handles errors. @@ -267,22 +336,22 @@ export function queryPbfAsGeoJSONOrArcGIS( * }); * ``` * - * @param requestOptions - Options for the request - * @returns A Promise that resolves with the feature by default, or with the native Response when `rawResponse` is `true`. + * @param requestOptions - Options for the request. + * @returns A Promise that resolves with the feature. */ export function getFeature( requestOptions: IGetFeatureOptions -): Promise { +): Promise { const url = `${cleanUrl(requestOptions.url)}/${requestOptions.id}`; // default to a GET request const options: IGetFeatureOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions.fetchOptions + } }; - if (options.rawResponse) { - return rawRequest(url, options); - } return request(url, options).then((response: any) => response.feature); } @@ -305,71 +374,29 @@ export function getFeature( export function queryFeatures( requestOptions: IQueryFeaturesOptions ): Promise { - const queryOptions = appendCustomParams( - requestOptions, - [ - "where", - "objectIds", - "relationParam", - "time", - "distance", - "units", - "outFields", - "geometry", - "geometryType", - "spatialRel", - "returnGeometry", - "maxAllowableOffset", - "geometryPrecision", - "inSR", - "outSR", - "gdbVersion", - "returnDistinctValues", - "returnIdsOnly", - "returnCountOnly", - "returnExtentOnly", - "orderByFields", - "groupByFieldsForStatistics", - "outStatistics", - "returnZ", - "returnM", - "multipatchOption", - "resultOffset", - "resultRecordCount", - "quantizationParameters", - "returnCentroid", - "resultType", - "historicMoment", - "returnTrueCurves", - "sqlFormat", - "returnExceededLimitFeatures", - "f" - ], - { - httpMethod: "GET", - params: { - // set default query parameters - where: "1=1", - outFields: "*", - ...requestOptions.params - } - } - ); - + const queryOptions = prepareQueryFeaturesOptions(requestOptions); if ( queryOptions.params?.f === "pbf-as-geojson" || queryOptions.params?.f === "pbf-as-arcgis" ) { return queryPbfAsGeoJSONOrArcGIS(requestOptions.url, queryOptions); - } else if (queryOptions.params?.f === "pbf") { - return rawRequest( - `${cleanUrl(requestOptions.url)}/query`, - queryOptions - ) as Promise; } return request(`${cleanUrl(requestOptions.url)}/query`, queryOptions); } +/** + * Query a feature service and return the native response. + * + * @param requestOptions - Options for the request + * @returns A Promise that resolves with the native response. + */ +export function queryFeaturesRaw( + requestOptions: IQueryFeaturesRawOptions +): Promise { + const queryOptions = prepareQueryFeaturesOptions(requestOptions); + return rawRequest(`${cleanUrl(requestOptions.url)}/query`, queryOptions); +} + /** * Query a feature service to retrieve all features. See [REST Documentation](https://developers.arcgis.com/rest/services-reference/query-feature-service-layer-.htm) for more information. * @@ -416,7 +443,7 @@ export async function queryAllFeatures( } else { // retrieve the maxRecordCount for the service only if user did not provide resultRecordCount const pageSizeResponse = await request(requestOptions.url, { - httpMethod: "GET", + fetchOptions: { method: "GET" }, authentication: requestOptions.authentication }); // default the pageSize to 2000 if it is not provided @@ -471,7 +498,6 @@ export async function queryAllFeatures( "f" ], { - httpMethod: "GET", params: { where: "1=1", outFields: "*", @@ -481,6 +507,11 @@ export async function queryAllFeatures( } ); + queryOptions.fetchOptions = { + method: "GET", + ...queryOptions.fetchOptions + }; + let response: IQueryAllFeaturesResponse; if ( queryOptions.params?.f === "pbf-as-geojson" || diff --git a/packages/arcgis-rest-feature-service/src/queryRelated.ts b/packages/arcgis-rest-feature-service/src/queryRelated.ts index 7a61a6912..61f960182 100644 --- a/packages/arcgis-rest-feature-service/src/queryRelated.ts +++ b/packages/arcgis-rest-feature-service/src/queryRelated.ts @@ -68,7 +68,6 @@ export function queryRelated( requestOptions, ["objectIds", "relationshipId", "definitionExpression", "outFields"], { - httpMethod: "GET", params: { // set default query parameters definitionExpression: "1=1", @@ -79,6 +78,11 @@ export function queryRelated( } ); + options.fetchOptions = { + method: "GET", + ...options.fetchOptions + }; + return request( `${cleanUrl(requestOptions.url)}/queryRelatedRecords`, options diff --git a/packages/arcgis-rest-feature-service/test/query.test.live.ts b/packages/arcgis-rest-feature-service/test/query.test.live.ts index c881cc6f2..448fcef3f 100644 --- a/packages/arcgis-rest-feature-service/test/query.test.live.ts +++ b/packages/arcgis-rest-feature-service/test/query.test.live.ts @@ -3,9 +3,11 @@ import { describe, afterEach, test, expect } from "vitest"; import { IQueryAllFeaturesOptions, IQueryFeaturesOptions, + IQueryFeaturesRawOptions, IQueryFeaturesResponse, queryAllFeatures, - queryFeatures + queryFeatures, + queryFeaturesRaw } from "../src/index.js"; import pbfToArcGIS from "../src/pbf-parser/arcGISPbfParser.js"; import { readEnvironmentFileToJSON } from "./utils/readFileArrayBuffer.js"; @@ -442,14 +444,14 @@ describe("queryFeatures() and queryAllFeatures() live tests", () => { }); test("LIVE TEST: should decode POINT pbf to arcgis", async () => { - const zipCodePointsPbfOptions: IQueryFeaturesOptions = { + const zipCodePointsPbfOptions: IQueryFeaturesRawOptions = { url: `https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_ZIP_Code_Points_analysis/FeatureServer/0`, f: "pbf", where: "1=1", outFields: ["*"], resultRecordCount: 1 }; - const response = await queryFeatures(zipCodePointsPbfOptions); + const response = await queryFeaturesRaw(zipCodePointsPbfOptions); const arrBuffer = await (response as any).arrayBuffer(); const arcgis = pbfToArcGIS(arrBuffer); @@ -483,14 +485,14 @@ describe("queryFeatures() and queryAllFeatures() live tests", () => { }); test("LIVE TEST: should decode LINE pbf to arcgis", async () => { - const trailsLinesPbfOptions: IQueryFeaturesOptions = { + const trailsLinesPbfOptions: IQueryFeaturesRawOptions = { url: `https://services3.arcgis.com/GVgbJbqm8hXASVYi/arcgis/rest/services/Trails/FeatureServer/0`, f: "pbf", where: "1=1", outFields: ["*"], resultRecordCount: 1 }; - const response = await queryFeatures(trailsLinesPbfOptions); + const response = await queryFeaturesRaw(trailsLinesPbfOptions); const arrBuffer = await (response as any).arrayBuffer(); const arcgis = pbfToArcGIS(arrBuffer); @@ -516,14 +518,14 @@ describe("queryFeatures() and queryAllFeatures() live tests", () => { }); test("LIVE TEST: should decode POLYGON pbf to arcgis", async () => { - const parksPolygonsPbfOptions: IQueryFeaturesOptions = { + const parksPolygonsPbfOptions: IQueryFeaturesRawOptions = { url: `https://services3.arcgis.com/GVgbJbqm8hXASVYi/ArcGIS/rest/services/Parks_and_Open_Space_Styled/FeatureServer/0`, f: "pbf", where: "1=1", outFields: ["*"], resultRecordCount: 1 }; - const response = await queryFeatures(parksPolygonsPbfOptions); + const response = await queryFeaturesRaw(parksPolygonsPbfOptions); const arrBuffer = await (response as any).arrayBuffer(); const arcgis = pbfToArcGIS(arrBuffer); // required properties diff --git a/packages/arcgis-rest-feature-service/test/query.test.ts b/packages/arcgis-rest-feature-service/test/query.test.ts index a58f4a204..1ecfaa8e3 100644 --- a/packages/arcgis-rest-feature-service/test/query.test.ts +++ b/packages/arcgis-rest-feature-service/test/query.test.ts @@ -6,9 +6,11 @@ import fetchMock from "fetch-mock"; import { getFeature, queryFeatures, + queryFeaturesRaw, queryAllFeatures, queryRelated, IQueryFeaturesOptions, + IQueryFeaturesRawOptions, IQueryRelatedOptions, IQueryAllFeaturesOptions, IQueryFeaturesResponse @@ -51,28 +53,6 @@ describe("getFeature() and queryFeatures()", () => { expect(response.attributes.FID).toBe(42); }); - test("return rawResponse when getting a feature", async () => { - const requestOptions = { - url: serviceUrl, - id: 42, - rawResponse: true - }; - fetchMock.once("*", featureResponse); - - const response: any = await getFeature(requestOptions); - - expect(fetchMock.called()).toBeTruthy(); - const [url, options] = fetchMock.lastCall("*"); - expect(url).toBe(`${requestOptions.url}/42?f=json`); - expect(options.method).toBe("GET"); - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - expect(response.body.Readable).not.toBe(null); - - const raw = await response.json(); - expect(raw).toEqual(featureResponse); - }); - test("should supply default query parameters", async () => { const requestOptions: IQueryFeaturesOptions = { url: serviceUrl @@ -108,6 +88,31 @@ describe("getFeature() and queryFeatures()", () => { expect(options.method).toBe("GET"); }); + test("queryFeaturesRaw should return raw response for default json queries", async () => { + const requestOptions: IQueryFeaturesOptions = { + url: serviceUrl, + where: "1=1", + outFields: ["*"] + }; + fetchMock.once("*", queryResponse); + + const response: any = await queryFeaturesRaw(requestOptions); + + expect(fetchMock.called()).toBeTruthy(); + const [url, options] = fetchMock.lastCall("*"); + expect(url).toEqual( + `${requestOptions.url}/query?f=json&where=1%3D1&outFields=*` + ); + expect(options.method).toBe("GET"); + + expect(response.status).toBe(200); + expect(response.ok).toBe(true); + + // convert the raw response to json and verify the json is as expected + const json = await response.json(); + expect(json.features[0].attributes.FID).toBe(1); + }); + test("should supply default query related parameters", async () => { const requestOptions: IQueryRelatedOptions = { url: serviceUrl @@ -129,7 +134,7 @@ describe("getFeature() and queryFeatures()", () => { relationshipId: 1, definitionExpression: "APPROXACRE<10000", outFields: ["APPROXACRE", "FIELD_NAME"], - httpMethod: "POST" + fetchOptions: { method: "POST" } }; fetchMock.once("*", queryRelatedResponse); const response = await queryRelated(requestOptions); @@ -1307,12 +1312,12 @@ describe("queryAllFeatures (custom pagination)", () => { }); }); -describe("queryFeatures(): pbf", () => { +describe("queryFeaturesRaw() and queryFeatures(): pbf", () => { afterEach(() => { fetchMock.restore(); }); - test("should return raw response for f=pbf without decoding", async () => { + test("queryFeaturesRaw should return raw response for f=pbf without decoding", async () => { const arrayBuffer = await readEnvironmentFileToArrayBuffer( "./packages/arcgis-rest-feature-service/test/mocks/pbf/CRS4326/PBFPointResponseCRS4326.pbf" ); @@ -1327,7 +1332,7 @@ describe("queryFeatures(): pbf", () => { { sendAsJson: false } ); - const requestOptions: IQueryFeaturesOptions = { + const requestOptions: IQueryFeaturesRawOptions = { url: serviceUrl, f: "pbf", where: "1=1", @@ -1335,7 +1340,7 @@ describe("queryFeatures(): pbf", () => { resultRecordCount: 1 }; - const response: any = await queryFeatures(requestOptions); + const response: any = await queryFeaturesRaw(requestOptions); expect(fetchMock.called()).toBeTruthy(); const [url, options] = fetchMock.lastCall("*"); @@ -1348,7 +1353,50 @@ describe("queryFeatures(): pbf", () => { expect(response.status).toBe(200); expect(response.ok).toBe(true); - const rawBuffer = await response.arrayBuffer(); - expect(rawBuffer.byteLength).toBeGreaterThan(0); + const rawBuffer = (await response.arrayBuffer()) as ArrayBuffer; + // expect the raw buffer to have a byte length of 443, which is the length of the mock pbf file + expect(rawBuffer.byteLength).toBe(443); + }); + + test("queryFeatures with f=pbf should warn, but will query json and return typed json for f=pbf queries", async () => { + // create console spy to check if warning is logged for f=pbf queries + const consoleWarn = console.warn; + const warnSpy = vi.fn(); + console.warn = warnSpy; + + fetchMock.once("*", { + features: [ + { + attributes: { name: "Feature 1" } + } + ], + exceededTransferLimit: true + }); + + const requestOptions: IQueryFeaturesRawOptions = { + url: serviceUrl, + f: "pbf", + where: "1=1", + outFields: ["*"], + resultRecordCount: 1 + }; + + const response = (await queryFeatures( + // typescript should warn here if user passes in f=pbf, but this test is asserting the behavior in which case they try and override it + requestOptions as any + )) as IQueryFeaturesResponse; + + expect(warnSpy).toHaveBeenCalledTimes(1); + // expect warn spy with message that includes + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + "request() only supports 'json' formats and responses. Provided value 'pbf' will be defaulted to 'json'. Use 'rawRequest()' to support special 'f' parameter values." + ) + ); + expect(response.features.length).toBe(1); + expect(response.features[0].attributes.name).toBe("Feature 1"); // dummy assertion to ensure test passes if no error is thrown + + // reset console.warn to default behavior + console.warn = consoleWarn; }); }); diff --git a/packages/arcgis-rest-geocoding/src/bulk.ts b/packages/arcgis-rest-geocoding/src/bulk.ts index 2dcfd0359..77d447bb8 100644 --- a/packages/arcgis-rest-geocoding/src/bulk.ts +++ b/packages/arcgis-rest-geocoding/src/bulk.ts @@ -68,9 +68,9 @@ export interface IBulkGeocodeResponse { * ``` * * @param requestOptions - Request options to pass to the geocoder, including an array of addresses and authentication session. - * @returns A Promise that will resolve with the data from the response. The spatial reference will be added to address locations unless `rawResponse: true` was passed. + * @returns A Promise that will resolve with the data from the response. The spatial reference will be added to address locations. */ -export function bulkGeocode( +export async function bulkGeocode( requestOptions: IBulkGeocodeOptions // must POST, which is the default ): Promise { const options: IBulkGeocodeOptions = { @@ -90,28 +90,20 @@ export function bulkGeocode( !requestOptions.authentication && options.endpoint === ARCGIS_ONLINE_BULK_GEOCODING_URL ) { - return Promise.reject( - "bulk geocoding using the ArcGIS service requires authentication" + throw new Error( + "bulk geocoding using the ArcGIS service requires authentication." ); } - if (options.rawResponse) { - return rawRequest( - `${cleanUrl(options.endpoint)}/geocodeAddresses`, - options - ); - } - - return request( + const response = await request( `${cleanUrl(options.endpoint)}/geocodeAddresses`, options - ).then((response) => { - const sr = response.spatialReference; - response.locations.forEach(function (address: { location: IPoint }) { - if (address.location) { - address.location.spatialReference = sr; - } - }); - return response; + ); + const sr = response.spatialReference; + response.locations.forEach(function (address: { location: IPoint }) { + if (address.location) { + address.location.spatialReference = sr; + } }); + return response; } diff --git a/packages/arcgis-rest-geocoding/src/geocode.ts b/packages/arcgis-rest-geocoding/src/geocode.ts index 97afc98af..6e87a0926 100644 --- a/packages/arcgis-rest-geocoding/src/geocode.ts +++ b/packages/arcgis-rest-geocoding/src/geocode.ts @@ -94,9 +94,9 @@ export interface IGeocodeResponse { * ``` * * @param address String representing the address or point of interest or RequestOptions to pass to the endpoint. - * @returns A Promise that will resolve with address candidates for the request. The spatial reference will be added to candidate locations and extents unless `rawResponse: true` was passed. + * @returns A Promise that will resolve with address candidates for the request. The spatial reference will be added to candidate locations and extents. */ -export function geocode( +export async function geocode( address: string | IGeocodeOptions ): Promise { let options: IGeocodeOptions = {}; @@ -135,47 +135,43 @@ export function geocode( } } - if (typeof address !== "string" && address.rawResponse) { - return rawRequest(`${cleanUrl(endpoint)}/findAddressCandidates`, options); - } - + const response = await request( + `${cleanUrl(endpoint)}/findAddressCandidates`, + options + ); + const sr: ISpatialReference = response.spatialReference; // add spatialReference property to individual matches - return request(`${cleanUrl(endpoint)}/findAddressCandidates`, options).then( - (response) => { - const sr: ISpatialReference = response.spatialReference; - response.candidates.forEach(function (candidate: { - location: IPoint; - extent?: IExtent; - }) { - candidate.location.spatialReference = sr; - if (candidate.extent) { - candidate.extent.spatialReference = sr; - } - }); + response.candidates.forEach(function (candidate: { + location: IPoint; + extent?: IExtent; + }) { + candidate.location.spatialReference = sr; + if (candidate.extent) { + candidate.extent.spatialReference = sr; + } + }); - // geoJson - if (sr.wkid === 4326) { - const features = response.candidates.map((candidate: any) => { - return { - type: "Feature", - geometry: arcgisToGeoJSON(candidate.location), - properties: Object.assign( - { - address: candidate.address, - score: candidate.score - }, - candidate.attributes - ) - }; - }); + // geoJson + if (sr.wkid === 4326) { + const features = response.candidates.map((candidate: any) => { + return { + type: "Feature", + geometry: arcgisToGeoJSON(candidate.location), + properties: Object.assign( + { + address: candidate.address, + score: candidate.score + }, + candidate.attributes + ) + }; + }); - response.geoJson = { - type: "FeatureCollection", - features - }; - } + response.geoJson = { + type: "FeatureCollection", + features + }; + } - return response; - } - ); + return response; } diff --git a/packages/arcgis-rest-geocoding/src/helpers.ts b/packages/arcgis-rest-geocoding/src/helpers.ts index 360f9d466..9e89f7151 100644 --- a/packages/arcgis-rest-geocoding/src/helpers.ts +++ b/packages/arcgis-rest-geocoding/src/helpers.ts @@ -47,9 +47,11 @@ export function getGeocodeService( (requestOptions && requestOptions.endpoint) || ARCGIS_ONLINE_GEOCODING_URL; const options: IEndpointOptions = { - httpMethod: "GET", - maxUrlLength: 2000, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return request(url, options); diff --git a/packages/arcgis-rest-geocoding/test/bulk.test.ts b/packages/arcgis-rest-geocoding/test/bulk.test.ts index e9a1a451a..2bf166883 100644 --- a/packages/arcgis-rest-geocoding/test/bulk.test.ts +++ b/packages/arcgis-rest-geocoding/test/bulk.test.ts @@ -66,8 +66,8 @@ describe("geocode", () => { test("should throw an error when a bulk geocoding request is made without a token", async () => { fetchMock.once("*", GeocodeAddresses); - await expect(bulkGeocode({ addresses })).rejects.toEqual( - "bulk geocoding using the ArcGIS service requires authentication" + await expect(bulkGeocode({ addresses })).rejects.toThrowError( + "bulk geocoding using the ArcGIS service requires authentication." ); }); @@ -179,34 +179,4 @@ describe("geocode", () => { ); expect(response.spatialReference.latestWkid).toEqual(4326); }); - - test("should support rawResponse", async () => { - fetchMock.once("*", GeocodeAddresses); - - const MOCK_AUTH = { - getToken() { - return Promise.resolve("token"); - }, - portal: "https://mapsdev.arcgis.com" - }; - - const response: any = await bulkGeocode({ - addresses, - authentication: MOCK_AUTH, - rawResponse: true - }); - expect(fetchMock.called()).toEqual(true); - const [url, options] = fetchMock.lastCall("*"); - expect(url).toEqual( - "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/geocodeAddresses" - ); - expect(options.method).toBe("POST"); - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - expect(response.body.Readable).not.toBe(null); - const raw = await response.json(); - expect(raw).toEqual(GeocodeAddresses); - // this used to work with isomorphic-fetch - // expect(response instanceof Response).toBe(true); - }); }); diff --git a/packages/arcgis-rest-geocoding/test/geocode.test.ts b/packages/arcgis-rest-geocoding/test/geocode.test.ts index ed69ecd99..b8c62da97 100644 --- a/packages/arcgis-rest-geocoding/test/geocode.test.ts +++ b/packages/arcgis-rest-geocoding/test/geocode.test.ts @@ -146,7 +146,7 @@ describe("geocode", () => { address: "380 New York St", postal: 92373 }, - httpMethod: "GET" + fetchOptions: { method: "GET" } }); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); @@ -177,36 +177,6 @@ describe("geocode", () => { ).toBe(true); }); - test("should support rawResponse", async () => { - fetchMock.once("*", FindAddressCandidates); - const response: any = await geocode({ - address: "1600 Pennsylvania Avenue", - city: "Washington D.C.", - rawResponse: true - }); - expect(fetchMock.called()).toEqual(true); - const [url, options] = fetchMock.lastCall("*"); - expect(url).toEqual( - "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates" - ); - expect(options.method).toBe("POST"); - expect(options.body).toContain("f=json"); - expect(options.body).toContain( - `address=${encodeURIComponent("1600 Pennsylvania Avenue")}` - ); - expect(options.body).toContain( - `city=${encodeURIComponent("Washington D.C.")}` - ); - expect(options.method).toBe("POST"); - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - expect(response.body.Readable).not.toBe(null); - const raw = await response.json(); - expect(raw).toEqual(FindAddressCandidates); - // this used to work with isomorphic-fetch - // expect(response instanceof Response).toBe(true); - }); - test("should make a single geocoding request with a postal code as a string", async () => { fetchMock.once("*", FindAddressCandidates); diff --git a/packages/arcgis-rest-geocoding/test/helpers.test.ts b/packages/arcgis-rest-geocoding/test/helpers.test.ts index 010cb3ad9..9b1159413 100644 --- a/packages/arcgis-rest-geocoding/test/helpers.test.ts +++ b/packages/arcgis-rest-geocoding/test/helpers.test.ts @@ -34,7 +34,9 @@ describe("geocode", () => { test("should make POST request for metadata from the World Geocoding Service", async () => { fetchMock.once("*", SharingInfo); - const response = await getGeocodeService({ httpMethod: "POST" }); + const response = await getGeocodeService({ + fetchOptions: { method: "POST" } + }); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); expect(url).toEqual( diff --git a/packages/arcgis-rest-geocoding/test/reverse.test.ts b/packages/arcgis-rest-geocoding/test/reverse.test.ts index 272ef7049..5685dc807 100644 --- a/packages/arcgis-rest-geocoding/test/reverse.test.ts +++ b/packages/arcgis-rest-geocoding/test/reverse.test.ts @@ -33,7 +33,7 @@ describe("geocode", () => { const response = await reverseGeocode( { x: -118.409, y: 33.9425, spatialReference: { wkid: 4326 } }, - { httpMethod: "GET" } + { fetchOptions: { method: "GET" } } ); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); diff --git a/packages/arcgis-rest-places/src/findPlacesNearPoint.ts b/packages/arcgis-rest-places/src/findPlacesNearPoint.ts index 80f668312..d487023d6 100644 --- a/packages/arcgis-rest-places/src/findPlacesNearPoint.ts +++ b/packages/arcgis-rest-places/src/findPlacesNearPoint.ts @@ -26,11 +26,18 @@ export interface IFindPlacesNearPointResponse extends successResponse { nextPage?: () => Promise; } +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode findPlacesNearPoint}. */ export interface IFindPlacesNearPointOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams { /** * Override the URL. This should be the full URL to the API endpoint you want to call. Used internally by Esri staff for testing. @@ -99,7 +106,10 @@ export function findPlacesNearPoint( return ( request(requestOptions.endpoint || `${baseUrl}/places/near-point`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } }) as Promise ).then((response) => { const r: IFindPlacesNearPointResponse = { diff --git a/packages/arcgis-rest-places/src/findPlacesWithinExtent.ts b/packages/arcgis-rest-places/src/findPlacesWithinExtent.ts index 3948920ba..71cdf5d9c 100644 --- a/packages/arcgis-rest-places/src/findPlacesWithinExtent.ts +++ b/packages/arcgis-rest-places/src/findPlacesWithinExtent.ts @@ -33,11 +33,18 @@ export interface IFindPlacesWithinExtentResponse extends successResponse { nextPage?: () => Promise; } +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode findPlacesNearPoint}. */ export interface IFindPlaceWithinExtentOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams { /** * Override the URL. This should be the full URL to the API endpoint you want to call. Used internally by Esri staff for testing. @@ -108,7 +115,10 @@ export function findPlacesWithinExtent( return ( request(requestOptions.endpoint || `${baseUrl}/places/within-extent`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } }) as Promise ).then((response) => { const r: IFindPlacesWithinExtentResponse = { diff --git a/packages/arcgis-rest-places/src/getCategories.ts b/packages/arcgis-rest-places/src/getCategories.ts index 1d4938485..8382c91f5 100644 --- a/packages/arcgis-rest-places/src/getCategories.ts +++ b/packages/arcgis-rest-places/src/getCategories.ts @@ -24,11 +24,18 @@ type successResponse = */ export interface IGetCategoriesResponse extends successResponse {} +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode getCategories}. */ export interface IGetCategoriesOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams { /** * Override the URL. This should be the full URL to the API endpoint you want to call. Used internally by Esri staff for testing. @@ -73,6 +80,9 @@ export function getCategories( return request(requestOptions.endpoint || `${baseUrl}/categories`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } }); } diff --git a/packages/arcgis-rest-places/src/getCategory.ts b/packages/arcgis-rest-places/src/getCategory.ts index 76daf5112..080464b66 100644 --- a/packages/arcgis-rest-places/src/getCategory.ts +++ b/packages/arcgis-rest-places/src/getCategory.ts @@ -21,11 +21,18 @@ type successResponse = */ export interface IGetCategoryResponse extends successResponse {} +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode getCategory}. */ export interface IGetCategoryOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams { categoryId: string; icon?: IconOptions; @@ -68,7 +75,10 @@ export function getCategory( requestOptions.endpoint || `${baseUrl}/categories/${categoryId}`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } } ); } diff --git a/packages/arcgis-rest-places/src/getPlaceDetails.ts b/packages/arcgis-rest-places/src/getPlaceDetails.ts index 89ff51d7b..f6feaefc3 100644 --- a/packages/arcgis-rest-places/src/getPlaceDetails.ts +++ b/packages/arcgis-rest-places/src/getPlaceDetails.ts @@ -24,11 +24,18 @@ type successResponse = */ export interface IGetPlaceResponse extends successResponse {} +type IRequestOptionsWithoutHttpMethod = Omit< + IRequestOptions, + "fetchOptions" +> & { + fetchOptions?: Omit; +}; + /** * Options for {@linkcode getPlaceDetails}. */ export interface IGetPlaceOptions - extends Omit, + extends IRequestOptionsWithoutHttpMethod, queryParams { placeId: string; /** @@ -82,6 +89,9 @@ export function getPlaceDetails( return request(requestOptions.endpoint || `${baseUrl}/places/${placeId}`, { ...options, - httpMethod: "GET" + fetchOptions: { + ...options.fetchOptions, + method: "GET" + } }); } diff --git a/packages/arcgis-rest-portal/src/groups/get.ts b/packages/arcgis-rest-portal/src/groups/get.ts index 926f42d6f..0437bba82 100644 --- a/packages/arcgis-rest-portal/src/groups/get.ts +++ b/packages/arcgis-rest-portal/src/groups/get.ts @@ -61,8 +61,11 @@ export function getGroup( const url = `${getPortalUrl(requestOptions)}/community/groups/${id}`; // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return request(url, options); } @@ -85,8 +88,11 @@ export function getGroupCategorySchema( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return request(url, options); } @@ -106,9 +112,12 @@ export function getGroupContent( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, params: { start: 1, num: 100 }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } } as IGetGroupContentOptions; // is this the most concise way to mixin with the defaults above? @@ -133,8 +142,11 @@ export function getGroupUsers( const url = `${getPortalUrl(requestOptions)}/community/groups/${id}/users`; // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return request(url, options); } @@ -180,10 +192,11 @@ export function searchGroupUsers( const url = `${getPortalUrl(searchOptions)}/community/groups/${id}/userlist`; const options = appendCustomParams( searchOptions || {}, - ["name", "num", "start", "sortField", "sortOrder", "joined", "memberType"], - { - httpMethod: "GET" - } + ["name", "num", "start", "sortField", "sortOrder", "joined", "memberType"] ); + options.fetchOptions = { + method: "GET", + ...options.fetchOptions + }; return request(url, options); } diff --git a/packages/arcgis-rest-portal/src/items/content.ts b/packages/arcgis-rest-portal/src/items/content.ts index ad8731dc3..c279d9b9a 100644 --- a/packages/arcgis-rest-portal/src/items/content.ts +++ b/packages/arcgis-rest-portal/src/items/content.ts @@ -58,7 +58,7 @@ export const getUserContent = ( ) .then((url) => request(url, { - httpMethod: "GET", + fetchOptions: { method: "GET" }, authentication, params: { start, diff --git a/packages/arcgis-rest-portal/src/items/export.ts b/packages/arcgis-rest-portal/src/items/export.ts index d0cd7ff28..7f8c645ce 100644 --- a/packages/arcgis-rest-portal/src/items/export.ts +++ b/packages/arcgis-rest-portal/src/items/export.ts @@ -80,7 +80,7 @@ export const exportItem = ( ) .then((url) => request(url, { - httpMethod: "POST", + fetchOptions: { method: "POST" }, authentication, params: { itemId, diff --git a/packages/arcgis-rest-portal/src/items/get.ts b/packages/arcgis-rest-portal/src/items/get.ts index fe5d6db46..04425ffc5 100644 --- a/packages/arcgis-rest-portal/src/items/get.ts +++ b/packages/arcgis-rest-portal/src/items/get.ts @@ -13,7 +13,6 @@ import { IItem } from "../helpers.js"; import { getPortalUrl } from "../util/get-portal-url.js"; import { scrubControlChars } from "../util/scrub-control-chars.js"; import { - IItemDataOptions, IItemRelationshipOptions, IUserItemOptions, determineOwner, @@ -45,8 +44,11 @@ export function getItem( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return request(url, options).then(async (item: IItem) => { @@ -100,20 +102,19 @@ export const getItemBaseUrl = ( */ export function getItemData( id: string, - requestOptions?: IItemDataOptions + requestOptions?: IRequestOptions ): Promise { const url = `${getItemBaseUrl(id, requestOptions)}/data`; // default to a GET request - const options: IItemDataOptions = { - ...{ httpMethod: "GET", params: {} }, - ...requestOptions + const options: IRequestOptions = { + params: {}, + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; - if (options.file) { - options.params.f = null; - return rawRequest(url, options).then((response) => response.blob()); - } - return request(url, options).catch((err) => { /* if the item doesn't include data, the response will be empty and the internal call to response.json() will fail */ @@ -126,6 +127,44 @@ export function getItemData( }); } +/** + * ``` + * import { getItemDataRaw } from "@esri/arcgis-rest-portal"; + * + * const response = await getItemDataRaw("ae7"); + * const data = await response.json(); + * // or + * const data = await response.text(); + * // or + * const data = await response.blob(); + * // or + * const data = await response.arrayBuffer(); + * ``` + * Get the native response for an item's /data resource so callers can parse + * JSON, text, blobs, or array buffers themselves. + * + * @param id - Item Id + * @param requestOptions - Options for the request + * @returns A Promise that will resolve with the native response. + */ +export function getItemDataRaw( + id: string, + requestOptions?: IRequestOptions +): Promise { + const url = `${getItemBaseUrl(id, requestOptions)}/data`; + const options: IRequestOptions = { + params: {}, + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } + }; + + options.params.f = null; + return rawRequest(url, options); +} + export interface IGetRelatedItemsResponse { total: number; relatedItems: IItem[]; @@ -155,11 +194,15 @@ export function getRelatedItems( )}/relatedItems`; const options: IItemRelationshipOptions = { - httpMethod: "GET", + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions.fetchOptions + }, params: { + ...requestOptions.params, direction: requestOptions.direction - }, - ...requestOptions + } }; if (typeof requestOptions.relationshipType === "string") { @@ -248,7 +291,7 @@ export interface IGetItemResourceOptions extends IRequestOptions { * .then(resourceContents => {}); * * // Get the response object instead - * getItemResource("3ef",{ rawResponse: true, fileName: "resource.json" }) + * getItemResource("3ef", { fileName: "resource.json" }) * .then(response => {}) * ``` * @@ -411,8 +454,11 @@ export function getItemInfo( ): Promise { const { fileName = "iteminfo.xml", readAs = "text" } = requestOptions || {}; const options: IRequestOptions = { - httpMethod: "GET", - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; return getItemFile(id, `/info/${fileName}`, readAs, options); } @@ -446,7 +492,7 @@ export function getItemMetadata( // overrides request()'s default behavior for reading the response // which is based on `params.f` and defaults to JSON // Also adds JSON parse error protection by sanitizing out any unescaped control characters before parsing -function getItemFile( +async function getItemFile( id: string, // NOTE: fileName should include any folder/subfolders fileName: string, @@ -454,23 +500,18 @@ function getItemFile( requestOptions?: IRequestOptions ): Promise { const url = `${getItemBaseUrl(id, requestOptions)}${fileName}`; - // preserve escape hatch to let the consumer read the response - // and ensure the f param is not appended to the query string + // ensure f param is not appended to the query string for file endpoints const options: IRequestOptions = { params: {}, ...requestOptions }; options.params.f = null; - if (options.rawResponse) { - return rawRequest(url, options); - } + const response = await rawRequest(url, options); - return rawRequest(url, options).then((response) => { - return readMethod !== "json" - ? response[readMethod]() - : response - .text() - .then((text: string) => JSON.parse(scrubControlChars(text))); - }); + return readMethod !== "json" + ? response[readMethod]() + : response + .text() + .then((text: string) => JSON.parse(scrubControlChars(text))); } diff --git a/packages/arcgis-rest-portal/src/items/helpers.ts b/packages/arcgis-rest-portal/src/items/helpers.ts index 5bbcdb712..66a1eae75 100644 --- a/packages/arcgis-rest-portal/src/items/helpers.ts +++ b/packages/arcgis-rest-portal/src/items/helpers.ts @@ -180,7 +180,7 @@ export interface ICreateUpdateItemOptions extends IAuthenticatedRequestOptions { export interface IItemDataOptions extends IRequestOptions { /** - * Used to request binary data. + * @deprecated Use getItemDataRaw() to retrieve native responses or binary item data. */ file?: boolean; } diff --git a/packages/arcgis-rest-portal/src/services/is-service-name-available.ts b/packages/arcgis-rest-portal/src/services/is-service-name-available.ts index 0e775ec7b..85927499d 100644 --- a/packages/arcgis-rest-portal/src/services/is-service-name-available.ts +++ b/packages/arcgis-rest-portal/src/services/is-service-name-available.ts @@ -22,7 +22,7 @@ export function isServiceNameAvailable( name, type }, - httpMethod: "GET", + fetchOptions: { method: "GET" }, authentication: session }); } diff --git a/packages/arcgis-rest-portal/src/sharing/is-item-shared-with-group.ts b/packages/arcgis-rest-portal/src/sharing/is-item-shared-with-group.ts index b9a678776..2cd542fd4 100644 --- a/packages/arcgis-rest-portal/src/sharing/is-item-shared-with-group.ts +++ b/packages/arcgis-rest-portal/src/sharing/is-item-shared-with-group.ts @@ -29,7 +29,7 @@ export function isItemSharedWithGroup( num: 10, sortField: "title", authentication: requestOptions.authentication, - httpMethod: "POST" + fetchOptions: { method: "POST" } } as ISearchOptions; return searchItems(searchOpts).then((searchResponse) => { diff --git a/packages/arcgis-rest-portal/src/users/get-user-properties.ts b/packages/arcgis-rest-portal/src/users/get-user-properties.ts index 79eec0708..ba806cbaa 100644 --- a/packages/arcgis-rest-portal/src/users/get-user-properties.ts +++ b/packages/arcgis-rest-portal/src/users/get-user-properties.ts @@ -34,7 +34,15 @@ export async function getUserProperties( const url = `${getPortalUrl( requestOptions )}/community/users/${encodeURIComponent(username)}/properties`; - const response = await request(url, { httpMethod: "GET", ...requestOptions }); + + const options = { + ...requestOptions, + fetchOptions: { + ...requestOptions.fetchOptions, + method: "GET" + } + }; + const response = await request(url, options); if (!response.properties.mapViewer) { response.properties.mapViewer = "modern"; } diff --git a/packages/arcgis-rest-portal/src/users/get-user.ts b/packages/arcgis-rest-portal/src/users/get-user.ts index eee8252df..bfe0f6872 100644 --- a/packages/arcgis-rest-portal/src/users/get-user.ts +++ b/packages/arcgis-rest-portal/src/users/get-user.ts @@ -41,7 +41,7 @@ export async function getUser( requestOptions?: string | IGetUserOptions ): Promise { let url; - let options = { httpMethod: "GET" } as IGetUserOptions; + let options = { fetchOptions: { method: "GET" } } as IGetUserOptions; // if a username is passed, assume ArcGIS Online if (typeof requestOptions === "string") { @@ -52,7 +52,10 @@ export async function getUser( url = `${getPortalUrl(requestOptions)}/community/users/${username}`; options = { ...requestOptions, - ...options + fetchOptions: { + ...requestOptions?.fetchOptions, + method: "GET" + } }; } // send the request diff --git a/packages/arcgis-rest-portal/src/users/invitation.ts b/packages/arcgis-rest-portal/src/users/invitation.ts index a7de0e146..995a2adcc 100644 --- a/packages/arcgis-rest-portal/src/users/invitation.ts +++ b/packages/arcgis-rest-portal/src/users/invitation.ts @@ -52,11 +52,16 @@ export interface IInvitationResult { export async function getUserInvitations( requestOptions: IAuthenticatedRequestOptions ): Promise { - let options = { httpMethod: "GET" } as IAuthenticatedRequestOptions; const username = await determineUsername(requestOptions); const portalUrl = getPortalUrl(requestOptions); const url = `${portalUrl}/community/users/${username}/invitations`; - options = { ...requestOptions, ...options }; + const options = { + ...requestOptions, + fetchOptions: { + ...requestOptions.fetchOptions, + method: "GET" + } + }; // send the request return request(url, options); @@ -90,8 +95,13 @@ export async function getUserInvitation( const portalUrl = getPortalUrl(requestOptions); const url = `${portalUrl}/community/users/${username}/invitations/${requestOptions.invitationId}`; - let options = { httpMethod: "GET" } as IGetUserInvitationOptions; - options = { ...requestOptions, ...options }; + const options = { + ...requestOptions, + fetchOptions: { + ...requestOptions.fetchOptions, + method: "GET" + } + }; // send the request return request(url, options); diff --git a/packages/arcgis-rest-portal/src/users/notification.ts b/packages/arcgis-rest-portal/src/users/notification.ts index 120877cf9..3f72550c2 100644 --- a/packages/arcgis-rest-portal/src/users/notification.ts +++ b/packages/arcgis-rest-portal/src/users/notification.ts @@ -46,12 +46,16 @@ export interface INotificationResult { export async function getUserNotifications( requestOptions: IAuthenticatedRequestOptions ): Promise { - let options = { httpMethod: "GET" } as IAuthenticatedRequestOptions; - const username = await determineUsername(requestOptions); const portalUrl = getPortalUrl(requestOptions); const url = `${portalUrl}/community/users/${username}/notifications`; - options = { ...requestOptions, ...options }; + const options = { + ...requestOptions, + fetchOptions: { + ...requestOptions.fetchOptions, + method: "GET" + } + }; // send the request return request(url, options); diff --git a/packages/arcgis-rest-portal/src/users/set-user-properties.ts b/packages/arcgis-rest-portal/src/users/set-user-properties.ts index 01f61c96c..fe2647510 100644 --- a/packages/arcgis-rest-portal/src/users/set-user-properties.ts +++ b/packages/arcgis-rest-portal/src/users/set-user-properties.ts @@ -24,7 +24,7 @@ export async function setUserProperties( requestOptions )}/community/users/${encodeURIComponent(username)}/setProperties`; const options: IAuthenticatedRequestOptions = { - httpMethod: "POST", + fetchOptions: { method: "POST" }, params: { properties }, ...requestOptions }; diff --git a/packages/arcgis-rest-portal/src/util/generic-search.ts b/packages/arcgis-rest-portal/src/util/generic-search.ts index 03fae12df..8491fbb68 100644 --- a/packages/arcgis-rest-portal/src/util/generic-search.ts +++ b/packages/arcgis-rest-portal/src/util/generic-search.ts @@ -29,7 +29,7 @@ export function genericSearch( let options: IRequestOptions; if (typeof search === "string" || search instanceof SearchQueryBuilder) { options = { - httpMethod: "GET", + fetchOptions: { method: "GET" }, params: { q: search } @@ -53,11 +53,16 @@ export function genericSearch( "categoryFilters" ], { - httpMethod: "GET" + fetchOptions: { method: "GET" } } ); } + options.fetchOptions = { + method: "GET", + ...options.fetchOptions + }; + let path; switch (searchType) { case "item": diff --git a/packages/arcgis-rest-portal/src/util/get-portal-settings.ts b/packages/arcgis-rest-portal/src/util/get-portal-settings.ts index 2461e811e..ea3c84e56 100644 --- a/packages/arcgis-rest-portal/src/util/get-portal-settings.ts +++ b/packages/arcgis-rest-portal/src/util/get-portal-settings.ts @@ -37,8 +37,11 @@ export function getPortalSettings( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; // send the request diff --git a/packages/arcgis-rest-portal/src/util/get-portal.ts b/packages/arcgis-rest-portal/src/util/get-portal.ts index e8bb08f83..da4177dd0 100644 --- a/packages/arcgis-rest-portal/src/util/get-portal.ts +++ b/packages/arcgis-rest-portal/src/util/get-portal.ts @@ -47,8 +47,11 @@ export function getPortal( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; // send the request diff --git a/packages/arcgis-rest-portal/src/util/get-subscription-info.ts b/packages/arcgis-rest-portal/src/util/get-subscription-info.ts index 45b20f264..66d4fa31b 100644 --- a/packages/arcgis-rest-portal/src/util/get-subscription-info.ts +++ b/packages/arcgis-rest-portal/src/util/get-subscription-info.ts @@ -36,8 +36,11 @@ export function getSubscriptionInfo( // default to a GET request const options: IRequestOptions = { - ...{ httpMethod: "GET" }, - ...requestOptions + ...requestOptions, + fetchOptions: { + method: "GET", + ...requestOptions?.fetchOptions + } }; // send the request diff --git a/packages/arcgis-rest-portal/test/items/get.test.ts b/packages/arcgis-rest-portal/test/items/get.test.ts index 25c5e895b..b3354a9c7 100644 --- a/packages/arcgis-rest-portal/test/items/get.test.ts +++ b/packages/arcgis-rest-portal/test/items/get.test.ts @@ -8,6 +8,7 @@ import { getItemBaseUrl, getItem, getItemData, + getItemDataRaw, getItemResources, getItemGroups, getItemStatus, @@ -36,8 +37,8 @@ import { } from "@esri/arcgis-rest-request"; import { - isBrowser, isNode, + isBrowser, TOMORROW } from "../../../../scripts/test-helpers.js"; @@ -77,7 +78,7 @@ describe("get", () => { expect(options.method).toBe("GET"); }); - test("should return binary item data by id", async () => { + test("should return raw item data by id", async () => { fetchMock.once( "*", { @@ -88,24 +89,39 @@ describe("get", () => { sendAsJson: false } ); - const response = await getItemData("3ef", { file: true }); + const response = await getItemDataRaw("3ef"); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); expect(url).toEqual( "https://www.arcgis.com/sharing/rest/content/items/3ef/data" ); expect(options.method).toBe("GET"); - expect(response).toBeDefined(); + expect(response.ok).toBe(true); + expect(response.status).toBe(200); + const blob = await response.blob(); if (isBrowser) { - expect(response).toBeInstanceOf(Blob); + expect(blob).toBeInstanceOf(Blob); } if (isNode) { - expect((response as Blob).size).toBe(4); - const bytes = new Uint8Array(await (response as Blob).arrayBuffer()); + expect(blob.size).toBe(4); + const bytes = new Uint8Array(await blob.arrayBuffer()); expect(Array.from(bytes)).toEqual([97, 98, 99, 100]); } }); + test("should return parsed json item data even when file is requested", async () => { + fetchMock.once("*", ItemDataResponse); + + const response = await getItemData("3ef", { file: true } as any); + expect(fetchMock.called()).toEqual(true); + const [url, options] = fetchMock.lastCall("*"); + expect(url).toEqual( + "https://www.arcgis.com/sharing/rest/content/items/3ef/data?f=json" + ); + expect(options.method).toBe("GET"); + expect(response).toEqual(ItemDataResponse); + }); + test("should return a valid response even when no data is retrieved", async () => { fetchMock.once( "*", @@ -175,22 +191,6 @@ describe("get", () => { expect(options.method).toBe("GET"); }); - test("should return raw response item info if desired", async () => { - fetchMock.once("*", ItemFormJsonResponse); - const response = await getItemInfo("3ef", { - fileName: "form.json", - rawResponse: true - } as IGetItemInfoOptions); - const formJson = await response.json(); - expect(formJson).toEqual(ItemFormJsonResponse); - expect(fetchMock.called()).toEqual(true); - const [url, options] = fetchMock.lastCall("*"); - expect(url).toEqual( - "https://www.arcgis.com/sharing/rest/content/items/3ef/info/form.json" - ); - expect(options.method).toBe("GET"); - }); - test("should return item info JSON files", async () => { fetchMock.once("*", ItemFormJsonResponse); const formJson = await getItemInfo("3ef", { @@ -403,24 +403,6 @@ describe("get", () => { expect(options.method).toBe("POST"); expect(resource.foo).toEqual("foobarbaz"); }); - - test("respects rawResponse setting with JSON resource", async () => { - const badJsonString = '{"foo":"foobarbaz"}'; - fetchMock.once("*", badJsonString); - - const response = await getItemResource("3ef", { - fileName: "resource.json", - rawResponse: true, - ...MOCK_USER_REQOPTS - }); - const [url, options] = fetchMock.lastCall("*"); - expect(url).toEqual( - "https://myorg.maps.arcgis.com/sharing/rest/content/items/3ef/resources/resource.json" - ); - expect(options.method).toBe("POST"); - expect(response.json).toBeDefined(); - await expect(response.json()).rejects.toBeDefined(); - }); }); test("get item groups anonymously", async () => { diff --git a/packages/arcgis-rest-portal/test/items/search.test.ts b/packages/arcgis-rest-portal/test/items/search.test.ts index 79bfe935c..07e5cec7d 100644 --- a/packages/arcgis-rest-portal/test/items/search.test.ts +++ b/packages/arcgis-rest-portal/test/items/search.test.ts @@ -140,7 +140,7 @@ describe("search", () => { start: 22, sortField: "title", sortOrder: "desc", - httpMethod: "POST" + fetchOptions: { method: "POST" } }); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); @@ -165,7 +165,7 @@ describe("search", () => { start: 22, sortField: "title", sortOrder: "desc", - httpMethod: "POST" + fetchOptions: { method: "POST" } }); expect(fetchMock.called()).toEqual(true); const [url, options] = fetchMock.lastCall("*"); diff --git a/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts b/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts index e7fe89201..f2a99ad0b 100644 --- a/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts +++ b/packages/arcgis-rest-request/src/ArcGISIdentityManager.ts @@ -672,7 +672,7 @@ export class ArcGISIdentityManager // exchange our auth code for a token + refresh token return fetchToken(tokenEndpoint, { - httpMethod: "POST", + fetchOptions: { method: "POST" }, params: { client_id: clientId, code_verifier: codeVerifier, @@ -1171,11 +1171,15 @@ export class ArcGISIdentityManager const url = `${this.portal}/portals/self`; const options = { - httpMethod: "GET", authentication: this, ...requestOptions } as IRequestOptions; + options.fetchOptions = { + method: "GET", + ...requestOptions?.fetchOptions + }; + this._pendingPortalRequest = request(url, options).then((response) => { this._portalInfo = response; this._pendingPortalRequest = null; diff --git a/packages/arcgis-rest-request/src/AuthenticationManagerBase.ts b/packages/arcgis-rest-request/src/AuthenticationManagerBase.ts index d6c9d24cf..223f99f16 100644 --- a/packages/arcgis-rest-request/src/AuthenticationManagerBase.ts +++ b/packages/arcgis-rest-request/src/AuthenticationManagerBase.ts @@ -86,11 +86,15 @@ class AuthenticationManagerBase { const url = `${this.portal}/community/self`; const options = { - httpMethod: "GET", authentication: this, ...requestOptions } as IRequestOptions; + options.fetchOptions = { + method: "GET", + ...requestOptions?.fetchOptions + }; + this._pendingUserRequest = request(url, options).then((response) => { this._user = response; this._pendingUserRequest = null; diff --git a/packages/arcgis-rest-request/src/request.ts b/packages/arcgis-rest-request/src/request.ts index 0e9668f5f..3a30dd198 100644 --- a/packages/arcgis-rest-request/src/request.ts +++ b/packages/arcgis-rest-request/src/request.ts @@ -656,7 +656,7 @@ export async function rawRequest( * .then(response) // response.currentVersion === 5.2 * * request('https://www.arcgis.com/sharing/rest', { - * httpMethod: "GET" + * fetchOptions: { method: "GET" } * }) * * request('https://www.arcgis.com/sharing/rest/search', { diff --git a/packages/arcgis-rest-request/src/revoke-token.ts b/packages/arcgis-rest-request/src/revoke-token.ts index 639ffa4d9..559c2d11a 100644 --- a/packages/arcgis-rest-request/src/revoke-token.ts +++ b/packages/arcgis-rest-request/src/revoke-token.ts @@ -48,7 +48,10 @@ export function revokeToken( const options: IRequestOptions = { ...requestOptions, - httpMethod: "POST", + fetchOptions: { + ...requestOptions.fetchOptions, + method: "POST" + }, params: { client_id: clientId, auth_token: token diff --git a/packages/arcgis-rest-request/src/utils/ITokenRequestOptions.ts b/packages/arcgis-rest-request/src/utils/ITokenRequestOptions.ts index 158094bcd..1dd5dab49 100644 --- a/packages/arcgis-rest-request/src/utils/ITokenRequestOptions.ts +++ b/packages/arcgis-rest-request/src/utils/ITokenRequestOptions.ts @@ -1,9 +1,7 @@ -import { HTTPMethods } from "./HTTPMethods.js"; +import { IRequestOptions } from "./IRequestOptions.js"; import { IGenerateTokenParams } from "./IGenerateTokenParams.js"; import { IFetchTokenParams } from "./IFetchTokenParams.js"; -export interface ITokenRequestOptions { +export interface ITokenRequestOptions extends IRequestOptions { params?: IGenerateTokenParams | IFetchTokenParams; - httpMethod?: HTTPMethods; - fetch?: (input: RequestInfo, init?: RequestInit) => Promise; }