Skip to content

Commit bee94ee

Browse files
new search api configuration props; adding tests for the query conversion functionality
1 parent c743072 commit bee94ee

File tree

10 files changed

+573
-56
lines changed

10 files changed

+573
-56
lines changed

src/dynamics-web-api.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ export type SearchEntity = {
915915
filter?: string;
916916
};
917917

918-
export type SearchOptions = {
918+
export type SearchOptions = Record<string, any> & {
919919
/**Values can be simple or lucene. */
920920
querytype?: "simple" | "lucene";
921921
/**Enables intelligent query workflow to return probable set of results if no good matches are found for the search request terms.*/
@@ -952,7 +952,7 @@ export interface Query extends SearchQueryBase {
952952
/**A list of comma-separated clauses where each clause consists of a column name followed by 'asc' (ascending, which is the default) or 'desc' (descending). This list specifies how to order the results in order of precedence. */
953953
orderBy?: string[];
954954
/**V2. Options are settings configured to search a search term. */
955-
options?: SearchOptions;
955+
options?: string | SearchOptions;
956956
/**
957957
* V1. Specifies whether any or all the search terms must be matched to count the document as a match. The default is 'any'.
958958
* @deprecated Use "options.searchmode".
@@ -1000,11 +1000,18 @@ export interface AutocompleteRequest extends BaseRequest {
10001000
query: Autocomplete;
10011001
}
10021002

1003-
export interface ApiConfig {
1004-
/** API Version to use, for example: "9.2" or "1.0" */
1003+
export type SearchApiOptions = {
1004+
/**Escapes the search string. Special characters that require escaping include the following: + - & | ! ( ) { } [ ] ^ " ~ * ? : \ /. */
1005+
escapeSpecialCharacters?: boolean;
1006+
}
1007+
1008+
export interface ApiConfig<TOptions = any> {
1009+
/** API Version to use, for example: "9.2" or "1.0". */
10051010
version?: string;
1006-
/** API Path, for example: "data" or "search" */
1011+
/** API Path, for example: "data" or "search". */
10071012
path?: string;
1013+
/** Specific API options. Currently it is only available for the Search API .*/
1014+
options?: TOptions;
10081015
}
10091016

10101017
export interface AccessToken {
@@ -1036,7 +1043,7 @@ export interface Config {
10361043
/**Configuration object for Dataverse Web API (with path "data"). */
10371044
dataApi?: ApiConfig;
10381045
/**Configuration object for Dataverse Search API (with path "search"). */
1039-
searchApi?: ApiConfig;
1046+
searchApi?: ApiConfig<SearchApiOptions>;
10401047
/**Default headers to supply with each request. */
10411048
headers?: HeaderCollection;
10421049
}

src/helpers/Regex.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,8 @@ export const FETCH_XML_PAGE_REGEX = /^<fetch.+page=/;
131131
export const FETCH_XML_REPLACE_REGEX = /^(<fetch)/;
132132

133133
export const DATE_FORMAT_REGEX = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:Z|[-+]\d{2}:\d{2})$/;
134+
135+
const SEARCH_SPECIAL_CHARACTERS_REGEX = /[+\-&|!(){}[\]^"~*?:\\\/]/g;
136+
export function escapeSearchSpecialCharacters(value: string): string {
137+
return value.replace(SEARCH_SPECIAL_CHARACTERS_REGEX, "\\$&");
138+
}

src/requests/search/autocomplete.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function autocomplete(request: string | AutocompleteRequest, client
2222

2323
internalRequest.functionName = internalRequest.collection = FUNCTION_NAME;
2424
internalRequest.method = "POST";
25-
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config);
25+
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config.searchApi);
2626
internalRequest.apiConfig = client.config.searchApi;
2727

2828
delete internalRequest.query;
Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
1-
import type { Autocomplete, Suggest, Query, SearchEntity } from "../../dynamics-web-api";
2-
import { InternalConfig } from "../../utils/Config";
1+
import type { Autocomplete, Suggest, Query, SearchEntity, SearchOptions } from "../../dynamics-web-api";
2+
import { escapeSearchSpecialCharacters } from "../../helpers/Regex";
3+
import { InternalApiConfig } from "../../utils/Config";
34

4-
export function convertSearchQuery(query: Query | Suggest | Autocomplete, functionName: string, config: InternalConfig) {
5+
type SearchApiFunction = "query" | "suggest" | "autocomplete";
6+
7+
export function convertSearchQuery(query: Query | Suggest | Autocomplete, functionName: SearchApiFunction, config: InternalApiConfig) {
58
if (!query) return query;
69

7-
query.entities = convertEntitiesProperty(query.entities, config.searchApi?.version);
10+
//escape special characters in a search query only if the option is set to true
11+
if (config?.escapeSpecialCharacters === true) {
12+
query.search = escapeSearchSpecialCharacters(query.search);
13+
}
14+
15+
if (query.entities?.length) {
16+
query.entities = convertEntitiesProperty(query.entities, config?.version);
17+
}
18+
819
if (functionName === "query") {
9-
query = convertQuery(query as Query, config.searchApi?.version);
20+
convertQuery(query as Query, config?.version);
1021
}
1122

1223
return query;
1324
}
1425

15-
function convertEntitiesProperty(entities?: string | string[] | SearchEntity[], version: string = "1.0"): string | string[] | SearchEntity[] | undefined {
26+
export function convertEntitiesProperty(entities?: string | string[] | SearchEntity[], version: string = "1.0"): string | string[] | undefined {
1627
if (!entities) return entities;
1728
if (typeof entities === "string") {
1829
if (version !== "1.0") return entities;
19-
throw new Error(
20-
"The 'entities' property must be an array of strings in the Search API v1.0. It cannot be a string. Or set Search API version to v2.0.",
21-
);
30+
try {
31+
entities = JSON.parse(entities) as SearchEntity[];
32+
} catch {
33+
throw new Error("The 'query.entities' property must be a valid JSON string.");
34+
}
35+
36+
if (!Array.isArray(entities)) {
37+
throw new Error("The 'query.entities' property must be an array of strings or objects.");
38+
}
2239
}
2340

2441
const toStringArray = (entity: string | SearchEntity) => {
@@ -31,54 +48,74 @@ function convertEntitiesProperty(entities?: string | string[] | SearchEntity[],
3148
return entity;
3249
};
3350

34-
return entities.map((entity: string | SearchEntity) => (version === "1.0" ? toStringArray(entity) : toSearchEntity(entity))) as string[] | SearchEntity[];
51+
const toReturn = entities.map((entity: string | SearchEntity) => (version === "1.0" ? toStringArray(entity) : toSearchEntity(entity)));
52+
53+
if (version !== "1.0") return JSON.stringify(toReturn);
54+
return toReturn as string[];
3555
}
3656

37-
function convertQuery(query: Query, version: string = "1.0") {
57+
export function convertQuery(query: Query, version: string = "1.0"): void {
3858
const toV1 = (query: Query) => {
3959
if (query.count != null) {
40-
query.returnTotalRecordCount = query.count;
60+
if (query.returnTotalRecordCount == null) {
61+
query.returnTotalRecordCount = query.count;
62+
}
4163
delete query.count;
4264
}
4365

4466
if (query.options) {
67+
if (typeof query.options === "string") {
68+
try {
69+
query.options = JSON.parse(query.options) as SearchOptions;
70+
} catch {
71+
throw new Error("The 'query.options' property must be a valid JSON string.");
72+
}
73+
}
74+
4575
if (!query.searchMode) {
4676
query.searchMode = query.options.searchmode;
4777
}
4878

4979
if (!query.searchType) {
50-
query.searchType = query.options.querytype === "simple" ? "simple" : "full";
80+
query.searchType = query.options.querytype === "lucene" ? "full" : query.options.querytype;
5181
}
5282

5383
delete query.options;
5484
}
55-
56-
return query;
5785
};
5886

5987
const toV2 = (query: Query) => {
6088
if (query.returnTotalRecordCount != null) {
61-
query.count = query.returnTotalRecordCount;
89+
if (query.count == null) {
90+
query.count = query.returnTotalRecordCount;
91+
}
6292
delete query.returnTotalRecordCount;
6393
}
6494

95+
6596
if (query.searchMode || query.searchType) {
66-
if (!query.options) query.options = {};
97+
//only set the options property if it's not a string
98+
if (typeof query.options !== "string") {
99+
if (!query.options) query.options = {};
67100

68-
if (!query.options.searchmode) {
69-
query.options.searchmode = query.searchMode;
70-
}
101+
if (!query.options.searchmode) {
102+
query.options.searchmode = query.searchMode;
103+
}
71104

72-
if (!query.options.querytype) {
73-
query.options.querytype = query.searchType === "simple" ? "simple" : "lucene";
105+
if (!query.options.querytype) {
106+
query.options.querytype = query.searchType === "full" ? "lucene" : query.searchType;
107+
}
74108
}
75109

76110
delete query.searchMode;
77111
delete query.searchType;
78112
}
79113

80-
return query;
114+
//convert options to string if it's an object
115+
if (query.options && typeof query.options !== "string") {
116+
query.options = JSON.stringify(query.options);
117+
}
81118
};
82119

83-
return version === "1.0" ? toV1(query) : toV2(query);
120+
version === "1.0" ? toV1(query) : toV2(query);
84121
}

src/requests/search/query.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { InternalRequest } from "../../types";
66
import { LIBRARY_NAME } from "../constants";
77
import { convertSearchQuery } from "./convertSearchQuery";
88

9-
const FUNCTION_NAME = "search";
9+
const FUNCTION_NAME = "query";
1010
const REQUEST_NAME = `${LIBRARY_NAME}.${FUNCTION_NAME}`;
1111

1212
export async function query<TValue = any>(request: string | QueryRequest, client: IDataverseClient): Promise<SearchResponse<TValue>> {
@@ -23,7 +23,7 @@ export async function query<TValue = any>(request: string | QueryRequest, client
2323
internalRequest.collection = "query";
2424
internalRequest.functionName = FUNCTION_NAME;
2525
internalRequest.method = "POST";
26-
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config);
26+
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config.searchApi);
2727
internalRequest.apiConfig = client.config.searchApi;
2828

2929
delete internalRequest.query;

src/requests/search/suggest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function suggest<TValueDocument = any>(request: string | SuggestReq
2222

2323
internalRequest.functionName = internalRequest.collection = FUNCTION_NAME;
2424
internalRequest.method = "POST";
25-
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config);
25+
internalRequest.data = convertSearchQuery(internalRequest.query, FUNCTION_NAME, client.config.searchApi);
2626
internalRequest.apiConfig = client.config.searchApi;
2727

2828
delete internalRequest.query;

src/utils/Config.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { isRunningWithinPortals, getClientUrl } from "./Utility";
22
import { ErrorHelper } from "../helpers/ErrorHelper";
3-
import { ApiConfig, Config } from "../dynamics-web-api";
3+
import { ApiConfig, Config, SearchApiOptions } from "../dynamics-web-api";
4+
import { LIBRARY_NAME } from "../requests/constants";
45

56
type ApiType = "dataApi" | "searchApi";
67

8+
const FUNCTION_NAME = `${LIBRARY_NAME}.setConfig`;
9+
710
export interface InternalApiConfig extends ApiConfig {
811
url: string;
12+
escapeSpecialCharacters?: boolean;
913
}
1014

1115
export interface InternalConfig extends Config {
@@ -26,65 +30,78 @@ const mergeApiConfigs = (apiConfig: ApiConfig | undefined, apiType: ApiType, int
2630
const internalApiConfig = internalConfig[apiType] as InternalApiConfig;
2731

2832
if (apiConfig?.version) {
29-
ErrorHelper.stringParameterCheck(apiConfig.version, "DynamicsWebApi.setConfig", `config.${apiType}.version`);
33+
ErrorHelper.stringParameterCheck(apiConfig.version, FUNCTION_NAME, `config.${apiType}.version`);
3034
internalApiConfig.version = apiConfig.version;
3135
}
3236

3337
if (apiConfig?.path) {
34-
ErrorHelper.stringParameterCheck(apiConfig.path, "DynamicsWebApi.setConfig", `config.${apiType}.path`);
38+
ErrorHelper.stringParameterCheck(apiConfig.path, FUNCTION_NAME, `config.${apiType}.path`);
3539
internalApiConfig.path = apiConfig.path;
3640
}
3741

42+
if (apiType === "searchApi") {
43+
mergeSearchApiOptions(apiConfig?.options, internalApiConfig);
44+
}
45+
3846
internalApiConfig.url = getApiUrl(internalConfig.serverUrl, internalApiConfig);
3947
};
4048

49+
const mergeSearchApiOptions = (options: SearchApiOptions | undefined, internalApiConfig: InternalApiConfig): void => {
50+
if (!options) return;
51+
52+
if (internalApiConfig.escapeSpecialCharacters != null) {
53+
ErrorHelper.boolParameterCheck(options.escapeSpecialCharacters, FUNCTION_NAME, `config.searchApi.options.escapeSpecialCharacters`);
54+
internalApiConfig.escapeSpecialCharacters = options.escapeSpecialCharacters;
55+
}
56+
}
57+
4158
export class ConfigurationUtility {
4259
static mergeApiConfigs = mergeApiConfigs;
4360

4461
static merge(internalConfig: InternalConfig, config?: Config): void {
4562
if (config?.serverUrl) {
46-
ErrorHelper.stringParameterCheck(config.serverUrl, "DynamicsWebApi.setConfig", "config.serverUrl");
63+
ErrorHelper.stringParameterCheck(config.serverUrl, FUNCTION_NAME, "config.serverUrl");
4764
internalConfig.serverUrl = config.serverUrl;
4865
}
4966

5067
mergeApiConfigs(config?.dataApi, "dataApi", internalConfig);
5168
mergeApiConfigs(config?.searchApi, "searchApi", internalConfig);
5269

5370
if (config?.impersonate) {
54-
internalConfig.impersonate = ErrorHelper.guidParameterCheck(config.impersonate, "DynamicsWebApi.setConfig", "config.impersonate");
71+
internalConfig.impersonate = ErrorHelper.guidParameterCheck(config.impersonate, FUNCTION_NAME, "config.impersonate");
5572
}
5673

5774
if (config?.impersonateAAD) {
58-
internalConfig.impersonateAAD = ErrorHelper.guidParameterCheck(config.impersonateAAD, "DynamicsWebApi.setConfig", "config.impersonateAAD");
75+
internalConfig.impersonateAAD = ErrorHelper.guidParameterCheck(config.impersonateAAD, FUNCTION_NAME, "config.impersonateAAD");
5976
}
6077

6178
if (config?.onTokenRefresh) {
62-
ErrorHelper.callbackParameterCheck(config.onTokenRefresh, "DynamicsWebApi.setConfig", "config.onTokenRefresh");
79+
ErrorHelper.callbackParameterCheck(config.onTokenRefresh, FUNCTION_NAME, "config.onTokenRefresh");
6380
internalConfig.onTokenRefresh = config.onTokenRefresh;
6481
}
6582

6683
if (config?.includeAnnotations) {
67-
ErrorHelper.stringParameterCheck(config.includeAnnotations, "DynamicsWebApi.setConfig", "config.includeAnnotations");
84+
ErrorHelper.stringParameterCheck(config.includeAnnotations, FUNCTION_NAME, "config.includeAnnotations");
6885
internalConfig.includeAnnotations = config.includeAnnotations;
6986
}
7087

7188
if (config?.timeout) {
72-
ErrorHelper.numberParameterCheck(config.timeout, "DynamicsWebApi.setConfig", "config.timeout");
89+
ErrorHelper.numberParameterCheck(config.timeout, FUNCTION_NAME, "config.timeout");
7390
internalConfig.timeout = config.timeout;
7491
}
7592

7693
if (config?.maxPageSize) {
77-
ErrorHelper.numberParameterCheck(config.maxPageSize, "DynamicsWebApi.setConfig", "config.maxPageSize");
94+
ErrorHelper.numberParameterCheck(config.maxPageSize, FUNCTION_NAME, "config.maxPageSize");
7895
internalConfig.maxPageSize = config.maxPageSize;
7996
}
8097

8198
if (config?.returnRepresentation) {
82-
ErrorHelper.boolParameterCheck(config.returnRepresentation, "DynamicsWebApi.setConfig", "config.returnRepresentation");
99+
ErrorHelper.boolParameterCheck(config.returnRepresentation, FUNCTION_NAME, "config.returnRepresentation");
83100
internalConfig.returnRepresentation = config.returnRepresentation;
84101
}
85102

86103
if (config?.useEntityNames) {
87-
ErrorHelper.boolParameterCheck(config.useEntityNames, "DynamicsWebApi.setConfig", "config.useEntityNames");
104+
ErrorHelper.boolParameterCheck(config.useEntityNames, FUNCTION_NAME, "config.useEntityNames");
88105
internalConfig.useEntityNames = config.useEntityNames;
89106
}
90107

@@ -93,15 +110,15 @@ export class ConfigurationUtility {
93110
}
94111

95112
if (!global.DWA_BROWSER && config?.proxy) {
96-
ErrorHelper.parameterCheck(config.proxy, "DynamicsWebApi.setConfig", "config.proxy");
113+
ErrorHelper.parameterCheck(config.proxy, FUNCTION_NAME, "config.proxy");
97114

98115
if (config.proxy.url) {
99-
ErrorHelper.stringParameterCheck(config.proxy.url, "DynamicsWebApi.setConfig", "config.proxy.url");
116+
ErrorHelper.stringParameterCheck(config.proxy.url, FUNCTION_NAME, "config.proxy.url");
100117

101118
if (config.proxy.auth) {
102-
ErrorHelper.parameterCheck(config.proxy.auth, "DynamicsWebApi.setConfig", "config.proxy.auth");
103-
ErrorHelper.stringParameterCheck(config.proxy.auth.username, "DynamicsWebApi.setConfig", "config.proxy.auth.username");
104-
ErrorHelper.stringParameterCheck(config.proxy.auth.password, "DynamicsWebApi.setConfig", "config.proxy.auth.password");
119+
ErrorHelper.parameterCheck(config.proxy.auth, FUNCTION_NAME, "config.proxy.auth");
120+
ErrorHelper.stringParameterCheck(config.proxy.auth.username, FUNCTION_NAME, "config.proxy.auth.username");
121+
ErrorHelper.stringParameterCheck(config.proxy.auth.password, FUNCTION_NAME, "config.proxy.auth.password");
105122
}
106123
}
107124

0 commit comments

Comments
 (0)