From 4b39e9e748e1c40be7eca4a81c913788f1edc28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Dostie?= <35579930+gdostie@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:46:01 -0400 Subject: [PATCH] fix(resource): allow empty string as query param value (#745) --- src/resources/Resource.ts | 10 +- src/resources/Search/Search.ts | 45 +++-- src/resources/Search/SearchInterfaces.ts | 229 +++++++---------------- src/resources/Search/test/Search.spec.ts | 20 +- 4 files changed, 126 insertions(+), 178 deletions(-) diff --git a/src/resources/Resource.ts b/src/resources/Resource.ts index 04df2e6f5..200749793 100644 --- a/src/resources/Resource.ts +++ b/src/resources/Resource.ts @@ -1,6 +1,8 @@ import API from '../APICore.js'; import queryString from '#query-string'; +const defaultOptions: queryString.StringifyOptions = {skipEmptyString: true, skipNull: true, sort: false}; + class Resource { static baseUrl: string; @@ -9,15 +11,15 @@ class Resource { protected serverlessApi: API, ) {} - protected buildPath(route: string, parameters?: any): string { - return route + this.convertObjectToQueryString(parameters); + protected buildPath(route: string, parameters?: any, options?: queryString.StringifyOptions): string { + return route + this.convertObjectToQueryString(parameters, options); } - private convertObjectToQueryString(parameters: any): string { + private convertObjectToQueryString(parameters: any, userOptions?: queryString.StringifyOptions): string { if (!parameters) { return ''; } else { - const requestURL = queryString.stringify(parameters, {skipEmptyString: true, skipNull: true, sort: false}); + const requestURL = queryString.stringify(parameters, {...defaultOptions, ...userOptions}); return requestURL.length ? `?${requestURL}` : ''; } } diff --git a/src/resources/Search/Search.ts b/src/resources/Search/Search.ts index fa21640b0..a429f6481 100644 --- a/src/resources/Search/Search.ts +++ b/src/resources/Search/Search.ts @@ -1,19 +1,19 @@ -import {ListFieldValuesBodyQueryParams, PostSearchQuerySuggestBodyParams} from './index.js'; import API from '../../APICore.js'; import Ressource from '../Resource.js'; import { - SingleItemParameters, ItemPreviewHtmlParameters, + RestFacetSearchParameters, + RestFacetSearchResponse, RestQueryParams, RestQueryResult, RestTokenParams, SearchListFieldsParams, SearchListFieldsResponse, SearchResponse, + SingleItemParameters, TokenModel, - RestFacetSearchParameters, - RestFacetSearchResponse, } from './SearchInterfaces.js'; +import {ListFieldValuesBodyQueryParams, PostSearchQuerySuggestBodyParams} from './index.js'; export default class Search extends Ressource { static baseUrl = `/rest/search/v2`; @@ -87,12 +87,29 @@ export default class Search extends Ressource { /** * Get an item's preview in HTML format */ - previewHTML(params: ItemPreviewHtmlParameters) { - return this.api.get( + previewHTML({ + enableNavigation, + findNext, + findPrevious, + organizationId, + page, + requestedOutputSize, + uniqueId, + viewAllContent, + ...body + }: ItemPreviewHtmlParameters) { + return this.api.post( this.buildPath(`${Search.baseUrl}/html`, { - ...params, - organizationId: params.organizationId ?? this.api.organizationId, + enableNavigation, + findNext, + findPrevious, + page, + requestedOutputSize, + uniqueId, + viewAllContent, + organizationId: organizationId ?? this.api.organizationId, }), + body, {responseBodyFormat: 'text'}, ); } @@ -102,10 +119,14 @@ export default class Search extends Ressource { */ getDocument(params: SingleItemParameters) { return this.api.get( - this.buildPath(`${Search.baseUrl}/document`, { - ...params, - organizationId: params.organizationId ?? this.api.organizationId, - }), + this.buildPath( + `${Search.baseUrl}/document`, + { + ...params, + organizationId: params.organizationId ?? this.api.organizationId, + }, + {skipEmptyString: false}, // otherwise we cannot use the empty pipeline (`pipeline=`) + ), ); } diff --git a/src/resources/Search/SearchInterfaces.ts b/src/resources/Search/SearchInterfaces.ts index 53ea658bd..0f03c4f9f 100644 --- a/src/resources/Search/SearchInterfaces.ts +++ b/src/resources/Search/SearchInterfaces.ts @@ -1,6 +1,6 @@ import {RestUserIdType} from '../Enums.js'; -export interface SearchListFieldsParams { +interface SharedSearchParams { /** * The unique identifier of the target Coveo Cloud organization. * Specifying a value for this parameter is only necessary when you are authenticating the API call with an OAuth2 token. @@ -9,9 +9,71 @@ export interface SearchListFieldsParams { /** * Whether to bypass document permissions. Only effective if the access token grants the Search - View all content privilege. */ - viewAllContent?: boolean; + viewAllContent?: boolean | number; + /** + * The name of the query pipeline to use for this request (bypassing its conditions, if it has any). + * + * You can pass an empty `pipeline` value to use an empty query pipeline (i.e., `?pipeline=` or `"pipeline": ""`). + * + * If a query does not contain the `pipeline` parameter, the first query pipeline whose conditions are met by the request is used (query pipelines without conditions are not evaluated). Should the request fail to meet the conditions of each evaluated query pipeline, the default query pipeline of the target Coveo Cloud organization is used (bypassing its conditions, if it has any). + * + * **Notes:** + * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `pipeline`, or a `searchHub` that routes queries to a specific `pipeline` via a query pipeline condition. + * - For reporting purposes, when logging a **Search** usage analytics event for a query, the `queryPipeline` field of that event should be set to the `pipeline` value of the query (or to the `"default"` string, if no `pipeline` value was specified in the query). + * + * See also [Managing Query Pipelines](https://docs.coveo.com/en/1450/). + * + * @example `CustomerQueryPipeline` + */ + pipeline?: string; + /** + * The first level of origin of the request, typically the identifier of the graphical search interface from which the request originates. + * + * Coveo Machine Learning models use this information to provide contextually relevant output. + * + * **Notes:** + * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `searchHub`. + * - When logging a **Search** usage analytics event for a query, the `originLevel1` field of that event should be set to the value of the `searchHub` search request parameter. + * + * See also the `tab` parameter. + * + * @example `CustomerPortal` + */ + searchHub?: string; + /** + * The [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) identifier of the time zone to use to correctly interpret dates in the query expression and result items. + * + * If not specified, the default time zone of the server hosting the index is used. + * + * **Note:** While no Coveo Machine Learning model uses this information, it can nevertheless affect the ranking scores (and thus, potentially the order) of result items, as ranking expressions may be based on time constants. + * + * @example `America/New_York` + */ + timezone?: string; + /** + * Whether to force a successful response to include debug information. + * + * **Notes:** + * - Debug information can only appear in responses in the JSON format (see the `format` parameter). + * - Avoid setting this parameter to `true` in production, as it has a negative impact on query performance. + * + * @default `false` + */ + debug?: boolean; + /** + * The identifier of the index mirror to forward the request to. See also the `indexToken` parameter. + * + * If you do not specify an `index` (or `indexToken`) value, any index mirror could be used. + * + * **Note:** Passing an `index` (or `indexToken`) value has no effect when the results of a specific request can be returned from cache (see the `maximumAge` parameter). + * + * @example `myorg-nvoqun-Indexer1-pbi2nbuw` + */ + index?: string; } +export interface SearchListFieldsParams extends Pick {} + export interface SearchListFieldsResponse { fields: Array<{ type: string; @@ -29,14 +91,12 @@ export interface SearchListFieldsResponse { }>; } -export interface RestTokenParams { +export interface RestTokenParams extends Pick { userIds: RestUserId[]; userGroups?: string[]; userDisplayName?: string; canSeeUserProfileOf?: string[]; - pipeline?: string; filter?: string; - searchHub?: string; salesforceOrganizationId?: string; validFor?: number; salesforceUser?: string; @@ -68,24 +128,14 @@ export interface TokenModel { token: string; } -export type RestQueryParams = PostSearchBodyQueryParams & PostSearchQueryStringParams; +export type RestQueryParams = PostSearchBodyQueryParams; -export interface PostSearchQueryStringParams { - /** - * The unique identifier of the target Coveo Cloud organization. - * Specifying a value for this parameter is only necessary when you are authenticating the API call with an OAuth2 token. - */ - organizationId?: string; - /** - * Whether to bypass document permissions. Only effective if the access token grants the Search - View all content privilege. - */ - viewAllContent?: boolean | number; -} +export interface PostSearchQueryStringParams extends Pick {} /** * Defines the body parameters of the list field values request. */ -export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringParams { +export interface ListFieldValuesBodyQueryParams extends SharedSearchParams { /** * Whether to treat accentuated characters as non-accentuated characters when retrieving field values (e.g., treat é, è, ê, etc., as e). * @@ -171,23 +221,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar */ dictionaryFieldContext?: RestDictionaryFieldContextRequest; - /** - * The name of the query pipeline to use for this request (bypassing its conditions, if it has any). - * - * You can pass an empty `pipeline` value to use an empty query pipeline (i.e., `?pipeline=` or `"pipeline": ""`). - * - * If a query does not contain the `pipeline` parameter, the first query pipeline whose conditions are met by the request is used (query pipelines without conditions are not evaluated). Should the request fail to meet the conditions of each evaluated query pipeline, the default query pipeline of the target Coveo Cloud organization is used (bypassing its conditions, if it has any). - * - * **Notes:** - * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `pipeline`, or a `searchHub` that routes queries to a specific `pipeline` via a query pipeline condition. - * - For reporting purposes, when logging a **Search** usage analytics event for a query, the `queryPipeline` field of that event should be set to the `pipeline` value of the query (or to the `"default"` string, if no `pipeline` value was specified in the query). - * - * See also [Managing Query Pipelines](https://docs.coveo.com/en/1450/). - * - * @example `CustomerQueryPipeline` - */ - pipeline?: string; - /** * The maximum age of cached results, in milliseconds. * @@ -199,21 +232,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar */ maximumAge?: number; - /** - * The first level of origin of the request, typically the identifier of the graphical search interface from which the request originates. - * - * Coveo Machine Learning models use this information to provide contextually relevant output. - * - * **Notes:** - * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `searchHub`. - * - When logging a **Search** usage analytics event for a query, the `originLevel1` field of that event should be set to the value of the `searchHub` search request parameter. - * - * See also the `tab` parameter. - * - * @example `CustomerPortal` - */ - searchHub?: string; - /** * The second level of origin of the request, typically the identifier of the selected tab in the graphical search interface from which the request originates. * @@ -289,17 +307,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar */ locale?: string; - /** - * The [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) identifier of the time zone to use to correctly interpret dates in the query expression and result items. - * - * If not specified, the default time zone of the server hosting the index is used. - * - * **Note:** While no Coveo Machine Learning model uses this information, it can nevertheless affect the ranking scores (and thus, potentially the order) of result items, as ranking expressions may be based on time constants. - * - * @example `America/New_York` - */ - timezone?: string; - /** * The format of a successful response. * @@ -313,17 +320,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar */ format?: RestFormat; - /** - * Whether to force a successful response to include debug information. - * - * **Notes:** - * - Debug information can only appear in responses in the JSON format (see the `format` parameter). - * - Avoid setting this parameter to `true` in production, as it has a negative impact on query performance. - * - * @default `false` - */ - debug?: boolean; - /** * The Base64 encoded identifier of the index mirror to forward the request to. See also the `index` parameter. * @@ -395,17 +391,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar */ indexType?: string; - /** - * The identifier of the index mirror to forward the request to. See also the `indexToken` parameter. - * - * If you do not specify an `index` (or `indexToken`) value, any index mirror could be used. - * - * **Note:** Passing an `index` (or `indexToken`) value has no effect when the results of a specific request can be returned from cache (see the `maximumAge` parameter). - * - * @example `myorg-nvoqun-Indexer1-pbi2nbuw` - */ - index?: string; - /** * The identifier for a logical group of indexes that have been configured to include documents form the same sources. * @@ -458,11 +443,6 @@ export interface ListFieldValuesBodyQueryParams extends PostSearchQueryStringPar * ``` */ analytics?: RestAnalyticsRequest; - - /** - * Whether to bypass document permissions. Only effective if the access token grants the Search - View all content privilege. - */ - viewAllContent?: boolean; } /** @@ -942,7 +922,7 @@ export interface PostSearchBodyQueryParams extends PostSearchBodyCommonParams { wildcards?: boolean; } -interface PostSearchBodyCommonParams { +interface PostSearchBodyCommonParams extends SharedSearchParams { /** * The query and page view actions previously made by the current user. * @@ -1008,17 +988,6 @@ interface PostSearchBodyCommonParams { */ context?: RestContextRequest; - /** - * Whether to force a successful response to include debug information. - * - * **Notes:** - * - Debug information can only appear in responses in the JSON format (see the `format` parameter). - * - Avoid setting this parameter to `true` in production, as it has a negative impact on query performance. - * - * @default `false` - */ - debug?: boolean; - /** * The format of a successful response. * @@ -1032,17 +1001,6 @@ interface PostSearchBodyCommonParams { */ format?: RestFormat; - /** - * The identifier of the index mirror to forward the request to. See also the `indexToken` parameter. - * - * If you do not specify an `index` (or `indexToken`) value, any index mirror could be used. - * - * **Note:** Passing an `index` (or `indexToken`) value has no effect when the results of a specific request can be returned from cache (see the `maximumAge` parameter). - * - * @example `myorg-nvoqun-Indexer1-pbi2nbuw` - */ - index?: string; - /** * The Base64 encoded identifier of the index mirror to forward the request to. See also the `index` parameter. * @@ -1136,23 +1094,6 @@ interface PostSearchBodyCommonParams { */ mlParameters?: Record; - /** - * The name of the query pipeline to use for this request (bypassing its conditions, if it has any). - * - * You can pass an empty `pipeline` value to use an empty query pipeline (i.e., `?pipeline=` or `"pipeline": ""`). - * - * If a query does not contain the `pipeline` parameter, the first query pipeline whose conditions are met by the request is used (query pipelines without conditions are not evaluated). Should the request fail to meet the conditions of each evaluated query pipeline, the default query pipeline of the target Coveo Cloud organization is used (bypassing its conditions, if it has any). - * - * **Notes:** - * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `pipeline`, or a `searchHub` that routes queries to a specific `pipeline` via a query pipeline condition. - * - For reporting purposes, when logging a **Search** usage analytics event for a query, the `queryPipeline` field of that event should be set to the `pipeline` value of the query (or to the `"default"` string, if no `pipeline` value was specified in the query). - * - * See also [Managing Query Pipelines](https://docs.coveo.com/en/1450/). - * - * @example `CustomerQueryPipeline` - */ - pipeline?: string; - /** * The basic query expression, typically the keywords entered by the end user in a query box. * @@ -1184,21 +1125,6 @@ interface PostSearchBodyCommonParams { */ referrer?: string; - /** - * The first level of origin of the request, typically the identifier of the graphical search interface from which the request originates. - * - * Coveo Machine Learning models use this information to provide contextually relevant output. - * - * **Notes:** - * - This parameter will be overridden if the search request is authenticated by a search token that enforces a specific `searchHub`. - * - When logging a **Search** usage analytics event for a query, the `originLevel1` field of that event should be set to the value of the `searchHub` search request parameter. - * - * See also the `tab` parameter. - * - * @example `CustomerPortal` - */ - searchHub?: string; - /** * The criteria to use for sorting the query results. * @@ -1233,17 +1159,6 @@ interface PostSearchBodyCommonParams { */ tab?: string; - /** - * The [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) identifier of the time zone to use to correctly interpret dates in the query expression and result items. - * - * If not specified, the default time zone of the server hosting the index is used. - * - * **Note:** While no Coveo Machine Learning model uses this information, it can nevertheless affect the ranking scores (and thus, potentially the order) of result items, as ranking expressions may be based on time constants. - * - * @example `America/New_York` - */ - timezone?: string; - /** * A GUID representing the current user, who can be authenticated or anonymous. This GUID is normally generated by the usage analytics service and stored in a non-expiring browser cookie. * @@ -2417,7 +2332,7 @@ export interface RestUserActionsParameters { tagViewsOfUser?: string; } -export interface ItemPreviewHtmlParameters extends PostSearchQueryStringParams { +export interface ItemPreviewHtmlParameters extends RestQueryParams { /** * The unique ID of the document. */ @@ -2917,14 +2832,14 @@ export interface SearchResponse { [key: string]: unknown; } -export interface SingleItemParameters extends PostSearchQueryStringParams { +export interface SingleItemParameters extends SharedSearchParams { /** * The unique ID of the document. */ uniqueId: string; } -export interface RestFacetSearchParameters extends PostSearchQueryStringParams { +export interface RestFacetSearchParameters extends Pick { /** * The name of the field against which to execute the facet search request. */ diff --git a/src/resources/Search/test/Search.spec.ts b/src/resources/Search/test/Search.spec.ts index 0f209a3c3..7adcbc815 100644 --- a/src/resources/Search/test/Search.spec.ts +++ b/src/resources/Search/test/Search.spec.ts @@ -161,11 +161,15 @@ describe('Search', () => { describe('previewHTML', () => { it('makes a GET call to the /html endpoint', () => { - search.previewHTML({uniqueId: 'document-id'}); - expect(api.get).toHaveBeenCalledTimes(1); - expect(api.get).toHaveBeenCalledWith(`/rest/search/v2/html?uniqueId=document-id`, { - responseBodyFormat: 'text', - }); + search.previewHTML({uniqueId: 'document-id', pipeline: ''}); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.post).toHaveBeenCalledWith( + `/rest/search/v2/html?uniqueId=document-id`, + {pipeline: ''}, + { + responseBodyFormat: 'text', + }, + ); }); }); @@ -175,6 +179,12 @@ describe('Search', () => { expect(api.get).toHaveBeenCalledTimes(1); expect(api.get).toHaveBeenCalledWith(`/rest/search/v2/document?uniqueId=document-id`); }); + + it('allows specifying the empty pipeline', () => { + search.getDocument({uniqueId: 'document-id', pipeline: ''}); + expect(api.get).toHaveBeenCalledTimes(1); + expect(api.get).toHaveBeenCalledWith(`/rest/search/v2/document?uniqueId=document-id&pipeline=`); + }); }); describe('searchFacet', () => {