diff --git a/packages/arcgis-rest-request/package-lock.json b/packages/arcgis-rest-request/package-lock.json new file mode 100644 index 00000000..b5cc51e0 --- /dev/null +++ b/packages/arcgis-rest-request/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=" + } + } +} diff --git a/packages/arcgis-rest-request/package.json b/packages/arcgis-rest-request/package.json new file mode 100644 index 00000000..deb66036 --- /dev/null +++ b/packages/arcgis-rest-request/package.json @@ -0,0 +1,38 @@ +{ + "name": "@esri/arcgis-rest-request", + "version": "1.0.0-alpha.2", + "description": "Common methods and utilities for @esri/arcgis-rest-* packages.", + "main": "dist/node/index.js", + "module": "dist/esm/index.js", + "js:next": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.7.1" + }, + "scripts": { + "prepublish": "npm run build", + "build": "npm run build:node && npm run build:esm", + "build:esm": "tsc --module es2015 --outDir ./dist/esm --declaration", + "build:node": "tsc --module commonjs --outDir ./dist/node", + "test": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Esri/arcgis-rest-js.git" + }, + "contributors": [ + { + "name": "Patrick Arlt", + "email": "parlt@esri.com", + "url": "http://patrickarlt.com/" + } + ], + "bugs": { + "url": "https://github.com/Esri/arcgis-rest-js/issues" + }, + "homepage": "https://github.com/Esri/arcgis-rest-js#readme", + "devDependencies": { + "typescript": "^2.6.2" + } +} diff --git a/packages/arcgis-rest-request/src/index.ts b/packages/arcgis-rest-request/src/index.ts new file mode 100644 index 00000000..03f9562d --- /dev/null +++ b/packages/arcgis-rest-request/src/index.ts @@ -0,0 +1,8 @@ +export * from "./request"; +export * from "./utils/encode-form-data"; +export * from "./utils/encode-query-string"; +export * from "./utils/check-for-errors"; +export * from "./utils/ArcGISRequestError"; +export * from "./utils/ArcGISAuthError"; +export * from "./utils/ErrorTypes"; +export * from "./utils/process-params"; diff --git a/packages/arcgis-rest-request/src/request.ts b/packages/arcgis-rest-request/src/request.ts new file mode 100644 index 00000000..f6b78d86 --- /dev/null +++ b/packages/arcgis-rest-request/src/request.ts @@ -0,0 +1,152 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { checkForErrors } from "./utils/check-for-errors"; +import { encodeFormData } from "./utils/encode-form-data"; +import { encodeQueryString } from "./utils/encode-query-string"; + +export interface IAuthenticationManager { + getToken(url: string): Promise; +} + +/** + * HTTP methods used by the ArcGIS REST API. + */ +export type HTTPMethods = "GET" | "POST"; + +/** + * Valid response formats for the `f` parameter. + */ +export type ResponseFormats = + | "json" + | "geojson" + | "text" + | "html" + | "image" + | "zip"; + +export interface IParams { + f?: ResponseFormats; + [key: string]: any; +} + +/** + * Options for the [`request()`](/api/arcgis-core/request/) method. + */ +export interface IRequestOptions { + /** + * The HTTP method to send the request with. + */ + httpMethod?: HTTPMethods; + + /** + * The instance of `IAuthenticationManager` to use to authenticate this request. + */ + authentication?: IAuthenticationManager; + + /** + * The implementation of `fetch` to use. Defaults to a global `fetch` + */ + fetch?: (input: RequestInfo, init?: RequestInit) => Promise; +} + +/** + * Generic method for making HTTP requests to ArcGIS REST API endpoints. + * + * ```js + * import { request } from 'arcgis-core'; + * + * request('https://www.arcgis.com/sharing/rest') + * .then((response) => { + * console.log(response.currentVersion); // => 5.2 + * }); + * ``` + * + * ```js + * import { request, HTTPMethods } from 'arcgis-core'; + * + * request('https://www.arcgis.com/sharing/rest', {}, { + * httpMethod: "GET" + * }).then((response) => { + * console.log(response.currentVersion); // => 5.2 + * }); + * ``` + * + * ```js + * import { request, HTTPMethods } from 'arcgis-core'; + * + * request('https://www.arcgis.com/sharing/rest/search', { + * q: 'parks' + * }).then((response) => { + * console.log(response.total); // => 78379 + * }); + * ``` + * + * @param url - The URL of the ArcGIS REST API endpoint. + * @param params - The parameters to pass to the endpoint. + * @param requestOptions - Options for the request. + * @returns A Promise that will resolve with the data from the request. + */ +export function request( + url: string, + requestParams: IParams = { f: "json" }, + requestOptions?: IRequestOptions +): Promise { + const options: IRequestOptions = { + ...{ httpMethod: "POST", fetch: fetch.bind(Function("return this")()) }, + ...requestOptions + }; + + const { httpMethod, authentication } = options; + + const params: IParams = { + ...{ f: "json" }, + ...requestParams + }; + + const fetchOptions: RequestInit = { + method: httpMethod + }; + + return (authentication ? authentication.getToken(url) : Promise.resolve("")) + .then(token => { + if (token.length) { + params.token = token; + } + + if (httpMethod === "GET") { + url = url + "?" + encodeQueryString(params); + } + + if (httpMethod === "POST") { + fetchOptions.body = encodeFormData(params); + } + + return options.fetch(url, fetchOptions); + }) + .then(response => { + switch (params.f) { + case "json": + return response.json(); + case "geojson": + return response.json(); + case "html": + return response.text(); + case "text": + return response.text(); + /* istanbul ignore next blob responses are difficult to make cross platform we will just have to trust the isomorphic fetch will do its job */ + case "image": + return response.blob(); + /* istanbul ignore next blob responses are difficult to make cross platform we will just have to trust the isomorphic fetch will do its job */ + case "zip": + return response.blob(); + } + }) + .then(data => { + if (params.f === "json" || params.f === "geojson") { + return checkForErrors(data, url, params, options); + } else { + return data; + } + }); +} diff --git a/packages/arcgis-rest-request/src/utils/ArcGISAuthError.ts b/packages/arcgis-rest-request/src/utils/ArcGISAuthError.ts new file mode 100644 index 00000000..dd91de79 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/ArcGISAuthError.ts @@ -0,0 +1,75 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { + request, + IRequestOptions, + IParams, + IAuthenticationManager +} from "../request"; +import { ArcGISRequestError } from "./ArcGISRequestError"; + +export type IRetryAuthError = ( + url: string, + params: IParams, + options: IRequestOptions +) => Promise; + +export class ArcGISAuthError extends ArcGISRequestError { + /** + * Create a new `ArcGISAuthError` object. + * + * @param message - The error message from the API + * @param code - The error code from the API + * @param response - The original response from the API that caused the error + * @param url - The original url of the request + * @param params - The original params of the request + * @param options - The original options of the request + */ + constructor( + message = "AUTHENTICATION_ERROR", + code: string | number = "AUTHENTICATION_ERROR_CODE", + response?: any, + url?: string, + params?: IParams, + options?: IRequestOptions + ) { + super(message, code, response, url, params, options); + this.name = "ArcGISAuthError"; + this.message = + code === "AUTHENTICATION_ERROR_CODE" ? message : `${code}: ${message}`; + } + + retry(getSession: IRetryAuthError, retryLimit = 3) { + let tries = 0; + + const retryRequest = (resolve: any, reject: any) => { + getSession(this.url, this.params, this.options) + .then(session => { + const newOptions = { + ...this.options, + ...{ authentication: session } + }; + + tries = tries + 1; + return request(this.url, this.params, newOptions); + }) + .then(response => { + resolve(response); + }) + .catch(e => { + if (e.name === "ArcGISAuthError" && tries < retryLimit) { + retryRequest(resolve, reject); + } else if (e.name === "ArcGISAuthError" && tries >= retryLimit) { + reject(this); + } else { + reject(e); + } + }); + }; + + return new Promise((resolve, reject) => { + retryRequest(resolve, reject); + }); + } +} diff --git a/packages/arcgis-rest-request/src/utils/ArcGISRequestError.ts b/packages/arcgis-rest-request/src/utils/ArcGISRequestError.ts new file mode 100644 index 00000000..39cdc27a --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/ArcGISRequestError.ts @@ -0,0 +1,81 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { IRequestOptions, IParams } from "../request"; + +// TypeScript 2.1 no longer allows you to extend built in types. See https://github.com/Microsoft/TypeScript/issues/12790#issuecomment-265981442 +// and https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work +// +// This code is from MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types. +export class ArcGISRequestError { + /** + * The name of this error. Will always be `"ArcGISRequestError"` to conform with the [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) class. + */ + name: string; + + /** + * Formatted error message. See the [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) class for more details. + */ + message: string; + + /** + * The errror message return from the request. + */ + originalMessage: string; + + /** + * The error code returned from the request. + */ + code: string | number; + + /** + * The original JSON response the caused the error. + */ + response: any; + + /** + * The URL of the original request that caused the error + */ + url: string; + + /** + * The parameters of the original request that caused the error + */ + params: IParams; + + /** + * The options of the original request that caused the error + */ + options: IRequestOptions; + + /** + * Create a new `ArcGISRequestError` object. + * + * @param message - The error message from the API + * @param code - The error code from the API + * @param response - The original response from the API that caused the error + * @param url - The original url of the request + * @param params - The original params of the request + * @param options - The original options of the request + */ + constructor( + message = "UNKNOWN_ERROR", + code: string | number = "UNKNOWN_ERROR_CODE", + response?: any, + url?: string, + params?: IParams, + options?: IRequestOptions + ) { + this.name = "ArcGISRequestError"; + this.message = + code === "UNKNOWN_ERROR_CODE" ? message : `${code}: ${message}`; + this.originalMessage = message; + this.code = code; + this.response = response; + this.url = url; + this.params = params; + this.options = options; + } +} +ArcGISRequestError.prototype = Object.create(Error.prototype); +ArcGISRequestError.prototype.constructor = ArcGISRequestError; diff --git a/packages/arcgis-rest-request/src/utils/ErrorTypes.ts b/packages/arcgis-rest-request/src/utils/ErrorTypes.ts new file mode 100644 index 00000000..fc9a6472 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/ErrorTypes.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +/** + * Enum describing the different errors that might be thrown by a request. + * + * ```ts + * import { request, ErrorTypes } from '@esri/arcgis-rest-request'; + * + * request("...").catch((e) => { + * switch(e.name) { + * case ErrorType.ArcGISRequestError: + * // handle a general error from the API + * break; + * + * case ErrorType.ArcGISAuthError: + * // handle an authentication error + * break; + * + * default: + * // handle some other error (usually a network error) + * } + * }); + * ``` + */ +export enum ErrorTypes { + ArcGISRequestError = "ArcGISRequestError", + ArcGISAuthError = "ArcGISAuthError" +} diff --git a/packages/arcgis-rest-request/src/utils/check-for-errors.ts b/packages/arcgis-rest-request/src/utils/check-for-errors.ts new file mode 100644 index 00000000..2a2a55c2 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/check-for-errors.ts @@ -0,0 +1,70 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { ArcGISRequestError } from "./ArcGISRequestError"; +import { ArcGISAuthError } from "./ArcGISAuthError"; +import { IRequestOptions, IParams } from "../request"; +/** + * Checks a JSON response from the ArcGIS REST API for errors. If there are no errors this will return the `data` it is passed in. If there is an error it will throw. With a [`ArcGISRequestError`](/api/arcgis-core/ArcGISRequestError/) or [`ArcGISAuthError`](/api/arcgis-core/ArcGISAuthError/). + * + * @param data The response JSON to check for errors. + * @param url The url of the original request + * @param params The parameters of the original request + * @param options The options of the original request + * @returns The data that was passed in the `data` parameter + */ +export function checkForErrors( + response: any, + url?: string, + params?: IParams, + options?: IRequestOptions +): any { + // this is an error message from billing.arcgis.com backend + if (response.code >= 400) { + const { message, code } = response; + throw new ArcGISRequestError(message, code, response, url, params, options); + } + + // error from ArcGIS Online or an ArcGIS Portal or server instance. + if (response.error) { + const { message, code, messageCode } = response.error; + const errorCode = messageCode || code || "UNKNOWN_ERROR_CODE"; + + if (code === 498 || code === 499 || messageCode === "GWM_0003") { + throw new ArcGISAuthError( + message, + errorCode, + response, + url, + params, + options + ); + } + + throw new ArcGISRequestError( + message, + errorCode, + response, + url, + params, + options + ); + } + + // error from a status check + if (response.status === "failed") { + let message: string; + let code: string = "UNKNOWN_ERROR_CODE"; + + try { + message = JSON.parse(response.statusMessage).message; + code = JSON.parse(response.statusMessage).code; + } catch (e) { + message = response.statusMessage; + } + + throw new ArcGISRequestError(message, code, response, url, params, options); + } + + return response; +} diff --git a/packages/arcgis-rest-request/src/utils/encode-form-data.ts b/packages/arcgis-rest-request/src/utils/encode-form-data.ts new file mode 100644 index 00000000..4ef26069 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/encode-form-data.ts @@ -0,0 +1,19 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { processParams } from "./process-params"; + +/** + * Encodes parameters in a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object in browsers or in a [FormData](https://github.com/form-data/form-data) in Node.js + * + * @param params An object to be encoded. + * @returns The complete [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. + */ +export function encodeFormData(params: any): FormData { + const formData = new FormData(); + const newParams = processParams(params); + Object.keys(newParams).forEach((key: any) => { + formData.append(key, newParams[key]); + }); + return formData; +} diff --git a/packages/arcgis-rest-request/src/utils/encode-query-string.ts b/packages/arcgis-rest-request/src/utils/encode-query-string.ts new file mode 100644 index 00000000..1a153c25 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/encode-query-string.ts @@ -0,0 +1,19 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +import { processParams } from "./process-params"; + +/** + * Encodes the passed object as a query string. + * + * @param params An object to be encoded. + * @returns An encoded query string. + */ +export function encodeQueryString(params: any): string { + const newParams = processParams(params); + return Object.keys(newParams) + .map((key: any) => { + return `${encodeURIComponent(key)}=${encodeURIComponent(newParams[key])}`; + }) + .join("&"); +} diff --git a/packages/arcgis-rest-request/src/utils/process-params.ts b/packages/arcgis-rest-request/src/utils/process-params.ts new file mode 100644 index 00000000..285a0834 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/process-params.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2017 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ + +/** + * Converts parameters to the proper representation to send to the ArcGIS REST API. + * @param params The object whose keys will be encoded. + * @return A new object with properly encoded values. + */ +export function processParams(params: any): any { + const newParams: any = {}; + + Object.keys(params).forEach(key => { + const param = params[key]; + const type = Object.prototype.toString.call(param); + let value: any; + + // properly encodes objects, arrays and dates for arcgis.com and other services. + // ported from https://github.com/Esri/esri-leaflet/blob/master/src/Request.js#L22-L30 + // also see https://github.com/Esri/arcgis-rest-js/issues/18 + switch (type) { + case "[object Array]": + value = + Object.prototype.toString.call(param[0]) === "[object Object]" + ? JSON.stringify(param) + : param.join(","); + break; + case "[object Object]": + value = JSON.stringify(param); + break; + case "[object Date]": + value = param.valueOf(); + break; + case "[object Function]": + value = null; + break; + case "[object Boolean]": + value = param + ""; + break; + default: + value = param; + break; + } + if (value) { + newParams[key] = value; + } + }); + + return newParams; +} diff --git a/packages/arcgis-rest-request/tsconfig.json b/packages/arcgis-rest-request/tsconfig.json new file mode 100644 index 00000000..9124bf20 --- /dev/null +++ b/packages/arcgis-rest-request/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es5", + "module": "es2015", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "noImplicitAny": false, + "outDir": "dist/esm", + "moduleResolution": "node", + "lib": ["es2017", "dom"] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/arcgis-rest-request/tslint.json b/packages/arcgis-rest-request/tslint.json new file mode 100644 index 00000000..176e7cf7 --- /dev/null +++ b/packages/arcgis-rest-request/tslint.json @@ -0,0 +1,31 @@ +{ + "extends": "tslint:latest", + "rules": { + "comment-format": [ + true, + "check-space" + ], + "indent": [ + true, + "spaces", + 2 + ], + "forin": true, + "no-duplicate-variable": true, + "no-eval": true, + "no-string-literal": false, + "semicolon": [ + true, + "never" + ], + "quotemark": [ + true, + "single" + ], + "object-literal-sort-keys": false, + "trailing-comma": false, + "max-line-length": false, + "no-shadowed-variable": false, + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"] + } +} diff --git a/packages/cedar/README.md b/packages/cedar/README.md index 0b9c2465..17b7bad5 100644 --- a/packages/cedar/README.md +++ b/packages/cedar/README.md @@ -180,6 +180,8 @@ To run tests continually for any package as you update it's soruce code, `cd` in Cedar currently uses the [amCharts JavaScripts Charts](https://www.amcharts.com/javascript-charts/) library as it's charting engine. You will need to include this along with cedar in your application. +Cedar supports the [same browsers as ArcGIS Online](https://doc.arcgis.com/en/arcgis-online/reference/browsers.htm), however you may need to include polyfills for [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) and [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), if your application has to support browers that don't support [fetch](https://caniuse.com/#search=fetch) or [Promise](https://caniuse.com/#search=promise) (i.e. IE or older versions of Safari/Android). + ### Versioning For transparency into the release cycle and in striving to maintain backward compatibility, Cedar is maintained under the Semantic Versioning guidelines and will adhere to these rules whenever possible. diff --git a/packages/cedar/package.json b/packages/cedar/package.json index 1c1d1253..92bbb5ef 100644 --- a/packages/cedar/package.json +++ b/packages/cedar/package.json @@ -61,7 +61,8 @@ }, "dependencies": { "amcharts3": "amcharts/amcharts3", - "@esri/cedar-amcharts": "^1.0.0-alpha.2" + "@esri/cedar-amcharts": "^1.0.0-alpha.2", + "@esri/arcgis-rest-request": "^1.0.0-alpha.2" }, "devDependencies": { "@types/jest": "20.0.2", diff --git a/packages/cedar/src/Chart.ts b/packages/cedar/src/Chart.ts index d2e49a0e..d433d8a3 100644 --- a/packages/cedar/src/Chart.ts +++ b/packages/cedar/src/Chart.ts @@ -1,7 +1,7 @@ import { cedarAmCharts, deepMerge } from '@esri/cedar-amcharts' import { flattenFeatures } from './flatten/flatten' import { getData } from './query/query' -import { createFeatureServiceRequest } from './query/url' +import { createQueryParams, getQueryUrl } from './query/url' function clone(json) { return JSON.parse(JSON.stringify(json)) @@ -152,7 +152,9 @@ export default class Chart { if (dataset.url) { // TODO: make name required on datasets, or required if > 1 dataset? names.push(dataset.name || `dataset${i}`) - requests.push(getData(createFeatureServiceRequest(dataset))) + const queryUrl = getQueryUrl(dataset) + const queryParams = createQueryParams(dataset.query) + requests.push(getData(queryUrl, queryParams)) } }) } diff --git a/packages/cedar/src/query/query.ts b/packages/cedar/src/query/query.ts index af9929d1..b13c139e 100644 --- a/packages/cedar/src/query/query.ts +++ b/packages/cedar/src/query/query.ts @@ -1,4 +1,7 @@ +import { request } from '@esri/arcgis-rest-request' + +// TODO: remove or at least rename fsToArr and arrToFs /** * Convert feature service response to an array * @param {object} fs Feature service response @@ -25,32 +28,6 @@ export function arrToFs(arr: any[]): any { } } -/** - * Handle FS errors && response - * @param {object} response Response from a fetch - * @return {Promise} Returns a promise that resolves as JSON - */ -export function checkStatusAndParseJson(response: any): Promise { - let err - if (response.status >= 200 && response.status < 300) { - // check if this is a 200, but really a 400 error - return response.json().then((json) => { - if (json.error) { - err = new Error(json.error.message) - err.code = json.error.code || 404 - err.response = response - throw err - } else { - return json - } - }) - } else { - // response has a non 200 http code - err = new Error(`Got ${response.status} ${response.statusText}`) - throw err - } -} - /** * Fetch data from a feature service * @param {string} url URL to fetch against @@ -59,12 +36,10 @@ export function checkStatusAndParseJson(response: any): Promise { */ export function getData(url: string, options?: any): Promise { const opts = options || {} - return fetch(url, opts) - .then((response) => { - return checkStatusAndParseJson(response) - }) + return request(url, opts) } +// TODO: remove default export export const query = { fsToArr, arrToFs, diff --git a/packages/cedar/src/query/url.ts b/packages/cedar/src/query/url.ts index 186b33cf..4b2c9a0d 100644 --- a/packages/cedar/src/query/url.ts +++ b/packages/cedar/src/query/url.ts @@ -13,63 +13,42 @@ export function defaultQuery() { } } -export function serializeQueryParams(params: any): string { - const str: string[] = [] - for (const param in params) { - if (params.hasOwnProperty(param)) { - let val = params[param] - if (typeof val !== 'string') { - val = JSON.stringify(val) - } - str.push(`${encodeURIComponent(param)}=${encodeURIComponent(val)}`) - } - } - return str.join('&') -} - -export function createFeatureServiceRequest(dataset: any): string { - const query = deepMerge({}, defaultQuery(), dataset.query) +export function createQueryParams(query: any = {}): any { + // merge in default query params + const queryParams = Object.assign(defaultQuery(), query) // Handle bbox - if (query.bbox) { + if (queryParams.bbox) { // make sure a geometry was not also passed in - if (query.geometry) { + if (queryParams.geometry) { throw new Error('Dataset.query can not have both a geometry and a bbox specified') } // Get the bbox (w,s,e,n) - const bboxArr = query.bbox.split(',') + const bboxArr = queryParams.bbox.split(',') // Remove it so it's not serialized as-is - delete query.bbox + delete queryParams.bbox // cook it into a json string - query.geometry = JSON.stringify({ - xmin: bboxArr[0], - ymin: bboxArr[2], - xmax: bboxArr[1], - ymax: bboxArr[3] - }) + queryParams.geometry = { + xmin: Number(bboxArr[0]), + ymin: Number(bboxArr[1]), + xmax: Number(bboxArr[2]), + ymax: Number(bboxArr[3]) + } // set spatial ref as geographic - query.inSR = '4326' + queryParams.inSR = '4326' } - if (!!query.outStatistics && typeof query.outStatistics !== 'string') { - query.outStatistics = JSON.stringify(query.outStatistics) - } + return queryParams +} - let builtUrl = `${dataset.url}/query?${serializeQueryParams(query)}` +export function getQueryUrl(dataset: any): string { + let builtUrl = `${dataset.url}/query?` if (dataset.token) { - builtUrl = `${builtUrl}&token=${dataset.token}` + builtUrl = `${builtUrl}token=${dataset.token}` } return builtUrl } - -export const url = { - defaultQuery, - serializeQueryParams, - createFeatureServiceRequest -} - -export default url diff --git a/packages/cedar/test/query/url.spec.ts b/packages/cedar/test/query/url.spec.ts index 917f2646..069bf09d 100644 --- a/packages/cedar/test/query/url.spec.ts +++ b/packages/cedar/test/query/url.spec.ts @@ -1,8 +1,24 @@ import {} from 'jest' -import url from '../../src/query/url' +import { createQueryParams, getQueryUrl } from '../../src/query/url' -describe('defaultQuery should match a basic default query', () => { - test('default query is...', () => { +describe('getQueryUrl', () => { + let dataset; + beforeEach(() => { + dataset = { + url: 'https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0' + } + }) + test('it should append query', () => { + expect(getQueryUrl(dataset)).toEqual('https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0/query?') + }) + test('it should append query and token', () => { + dataset.token = 'notarealtoken'; + expect(getQueryUrl(dataset)).toEqual('https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0/query?token=notarealtoken') + }) +}) + +describe('createQueryParams', () => { + test('should return default query params when no query is passed', () => { const defQuery = { where: '1=1', returnGeometry: false, @@ -13,34 +29,49 @@ describe('defaultQuery should match a basic default query', () => { sqlFormat: 'standard', f: 'json' } - const q = url.defaultQuery() - expect(q).toEqual(defQuery) + expect(createQueryParams()).toEqual(defQuery) }) -}) -describe('createFeatureServiceRequest creates a proper url string', () => { - test('A basic url is created', () => { - const dataset = { - url: 'https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0' - } - const result = 'https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0/query?where=1%3D1&returnGeometry=false&returnDistinctValues=false&returnIdsOnly=false&returnCountOnly=false&outFields=*&sqlFormat=standard&f=json' - expect(url.createFeatureServiceRequest(dataset)).toEqual(result) - }) - - test('A query is properly constructed', () => { + test('should merge defaults into query params, convert bbox into geometry, and copy outStatistics', () => { const dataset = { url: 'https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0', query: { + // bbox W,S,E,N + bbox: '-104,35.6,-94.32,41', groupByFieldsForStatistics: 'Type', orderByFields: 'Number_of_SUM DESC', outStatistics: [{ statisticType: 'sum', onStatisticField: 'Number_of', outStatisticFieldName: 'Number_of_SUM' - }] + }], + where: 'Type=\'High School\'' } } - const result = 'https://services.arcgis.com/uDTUpUPbk8X8mXwl/arcgis/rest/services/Public_Schools_in_Onondaga_County/FeatureServer/0/query?where=1%3D1&returnGeometry=false&returnDistinctValues=false&returnIdsOnly=false&returnCountOnly=false&outFields=*&sqlFormat=standard&f=json&groupByFieldsForStatistics=Type&orderByFields=Number_of_SUM%20DESC&outStatistics=%5B%7B%22statisticType%22%3A%22sum%22%2C%22onStatisticField%22%3A%22Number_of%22%2C%22outStatisticFieldName%22%3A%22Number_of_SUM%22%7D%5D' - expect(url.createFeatureServiceRequest(dataset)).toEqual(result) + const result = { + where: 'Type=\'High School\'', + returnGeometry: false, + returnDistinctValues: false, + returnIdsOnly: false, + returnCountOnly: false, + outFields: '*', + sqlFormat: 'standard', + f: 'json', + groupByFieldsForStatistics: 'Type', + orderByFields: 'Number_of_SUM DESC', + outStatistics: [{ + statisticType: 'sum', + onStatisticField: 'Number_of', + outStatisticFieldName: 'Number_of_SUM' + }], + geometry: { + xmin: -104, + ymin: 35.6, + xmax: -94.32, + ymax: 41 + }, + inSR: '4326' + } + expect(createQueryParams(dataset.query)).toEqual(result) }) })