diff --git a/packages/arcgis-rest-items/src/SearchBuilder.ts b/packages/arcgis-rest-items/src/SearchBuilder.ts new file mode 100644 index 0000000000..de3cecf8c2 --- /dev/null +++ b/packages/arcgis-rest-items/src/SearchBuilder.ts @@ -0,0 +1,112 @@ +import { IParamBuilder } from "@esri/arcgis-rest-request"; + +export class SearchQueryBuilder implements IParamBuilder { + private termStack: any[] = []; + private rangeStack: any[] = []; + private q: string; + + constructor(q: string = "") { + this.q = q; + } + + public match(...terms: any[]) { + this.termStack = this.termStack.concat(terms); + return this; + } + + public in(field: string) { + if (field && field !== "*") { + this.q += `${field}: `; + } + this.commit(); + return this; + } + + public startGroup() { + this.commit(); + this.q += "("; + return this; + } + + public endGroup() { + this.q += ")"; + return this; + } + + public and() { + this.commit(); + this.q += " AND "; + return this; + } + + public or() { + this.commit(); + this.q += " OR "; + return this; + } + + public not() { + this.commit(); + this.q += " NOT "; + return this; + } + + public from(term: any) { + this.rangeStack[0] = term; + return this; + } + + public to(term: any) { + this.rangeStack[1] = term; + return this; + } + + public boost(num: number) { + this.commit(); + this.q += "^${num}"; + return this; + } + + public toParam() { + return this.q; + } + + public clone() { + return new SearchQueryBuilder(this.q + ""); + } + + private hasWhiteSpace(s: string) { + return /\s/g.test(s); + } + + private formatTerm(term: any) { + if (term instanceof Date) { + return term.getTime(); + } + + if (typeof term === "string" && this.hasWhiteSpace(term)) { + return `"${term}"`; + } + + return term; + } + + private commit() { + if (this.rangeStack.length && this.rangeStack[0] && this.rangeStack[1]) { + this.q += `[${this.formatTerm(this.rangeStack[0])} TO ${this.formatTerm( + this.rangeStack[1] + )}]`; + this.rangeStack = [undefined, undefined]; + } + + if (this.termStack.length) { + this.q += this.termStack + .map(term => { + return term; + }) + .join(" "); + } + + this.termStack = []; + } +} diff --git a/packages/arcgis-rest-items/src/search.ts b/packages/arcgis-rest-items/src/search.ts index aa4c0dacd3..77dead8528 100644 --- a/packages/arcgis-rest-items/src/search.ts +++ b/packages/arcgis-rest-items/src/search.ts @@ -8,10 +8,11 @@ import { } from "@esri/arcgis-rest-request"; import { IPagingParams, IItem } from "@esri/arcgis-rest-common-types"; +import { SearchQueryBuilder } from "./SearchBuilder"; // this interface still needs to be docced export interface ISearchRequest extends IPagingParams { - q: string; + q: string | SearchQueryBuilder; [key: string]: any; } @@ -29,6 +30,7 @@ export interface ISearchResult { num: number; nextStart: number; results: IItem[]; + nextPage?: () => Promise; } /** @@ -44,14 +46,14 @@ export interface ISearchResult { * @returns A Promise that will resolve with the data from the response. */ export function searchItems( - search: string | ISearchRequestOptions + search: string | ISearchRequestOptions | SearchQueryBuilder ): Promise { let options: ISearchRequestOptions = { httpMethod: "GET", params: {} }; - if (typeof search === "string") { + if (typeof search === "string" || search instanceof SearchQueryBuilder) { options.params.q = search; } else { // mixin user supplied requestOptions with defaults @@ -71,5 +73,26 @@ export function searchItems( const url = `${getPortalUrl(options)}/search`; // send the request - return request(url, options); + return request(url, options).then(r => { + if (options.rawResponse) { + return r; + } + + if (r.nextStart === -1) { + r.nextPage = function() { + const newOptions = { + ...options, + ...{ + params: { + ...options.params, + ...{ start: r.nextStart } + } + } + }; + return request(url, newOptions); + }; + } + + return r; + }); } diff --git a/packages/arcgis-rest-request/src/index.ts b/packages/arcgis-rest-request/src/index.ts index 336d613097..993deb5d12 100644 --- a/packages/arcgis-rest-request/src/index.ts +++ b/packages/arcgis-rest-request/src/index.ts @@ -9,6 +9,7 @@ export * from "./utils/ArcGISRequestError"; export * from "./utils/retryAuthError"; export * from "./utils/ErrorTypes"; export * from "./utils/params"; +export * from "./utils/IParamBuilder"; export * from "./utils/process-params"; export * from "./utils/get-portal"; export * from "./utils/get-portal-url"; diff --git a/packages/arcgis-rest-request/src/utils/IParamBuilder.ts b/packages/arcgis-rest-request/src/utils/IParamBuilder.ts new file mode 100644 index 0000000000..8a9ad7daf7 --- /dev/null +++ b/packages/arcgis-rest-request/src/utils/IParamBuilder.ts @@ -0,0 +1,3 @@ +export interface IParamBuilder { + toParam(): any; +} diff --git a/packages/arcgis-rest-request/src/utils/process-params.ts b/packages/arcgis-rest-request/src/utils/process-params.ts index 6dc378d777..376dd40324 100644 --- a/packages/arcgis-rest-request/src/utils/process-params.ts +++ b/packages/arcgis-rest-request/src/utils/process-params.ts @@ -8,12 +8,16 @@ */ export function requiresFormData(params: any) { return Object.keys(params).some(key => { - const value = params[key]; + let value = params[key]; if (!value) { return false; } + if (value.toParam) { + value = value.toParam(); + } + const type = value.constructor.name; switch (type) { @@ -46,7 +50,12 @@ export function processParams(params: any): any { const newParams: any = {}; Object.keys(params).forEach(key => { - const param = params[key]; + let param = params[key]; + + if (param.toParams) { + param = param.toParam(); + } + if ( !param && param !== 0 &&