diff --git a/src/batch.ts b/src/batch.ts index 67fbc5695..32babced7 100644 --- a/src/batch.ts +++ b/src/batch.ts @@ -5,7 +5,7 @@ import type { BatchesResults, BatchesResultsObject, } from "./types.js"; -import { HttpRequests, toQueryParams } from "./http-requests.js"; +import { HttpRequests } from "./http-requests.js"; class Batch { uid: BatchObject["uid"]; @@ -41,8 +41,9 @@ class BatchClient { * @returns */ async getBatch(uid: number): Promise { - const url = `batches/${uid}`; - const batch = await this.httpRequest.get(url); + const batch = await this.httpRequest.get({ + path: `batches/${uid}`, + }); return new Batch(batch); } @@ -52,13 +53,11 @@ class BatchClient { * @param parameters - Parameters to browse the batches * @returns Promise containing all batches */ - async getBatches(parameters: BatchesQuery = {}): Promise { - const url = `batches`; - - const batches = await this.httpRequest.get>( - url, - toQueryParams(parameters), - ); + async getBatches(batchesQuery?: BatchesQuery): Promise { + const batches = await this.httpRequest.get({ + path: "batches", + params: batchesQuery, + }); return { ...batches, diff --git a/src/http-requests.ts b/src/http-requests.ts index 19e466323..87d6c8df0 100644 --- a/src/http-requests.ts +++ b/src/http-requests.ts @@ -1,165 +1,265 @@ -import type { Config, EnqueuedTaskObject } from "./types.js"; +import type { + Config, + HttpRequestsRequestInit, + RequestOptions, + MainRequestOptions, + URLSearchParamsRecord, +} from "./types.js"; import { PACKAGE_VERSION } from "./package-version.js"; - import { MeiliSearchError, MeiliSearchApiError, MeiliSearchRequestError, } from "./errors/index.js"; - -import { addTrailingSlash, addProtocolIfNotPresent } from "./utils.js"; - -type queryParams = { [key in keyof T]: string }; - -function toQueryParams(parameters: T): queryParams { - const params = Object.keys(parameters) as Array; - - const queryParams = params.reduce>((acc, key) => { - const value = parameters[key]; - if (value === undefined) { - return acc; - } else if (Array.isArray(value)) { - return { ...acc, [key]: value.join(",") }; - } else if (value instanceof Date) { - return { ...acc, [key]: value.toISOString() }; +import { addProtocolIfNotPresent, addTrailingSlash } from "./utils.js"; + +/** Append a set of key value pairs to a {@link URLSearchParams} object. */ +function appendRecordToURLSearchParams( + searchParams: URLSearchParams, + recordToAppend: URLSearchParamsRecord, +): void { + for (const [key, val] of Object.entries(recordToAppend)) { + if (val != null) { + searchParams.set( + key, + Array.isArray(val) + ? val.join() + : val instanceof Date + ? val.toISOString() + : String(val), + ); } - return { ...acc, [key]: value }; - }, {} as queryParams); - return queryParams; -} - -function constructHostURL(host: string): string { - try { - host = addProtocolIfNotPresent(host); - host = addTrailingSlash(host); - return host; - } catch { - throw new MeiliSearchError("The provided host is not valid."); } } -function cloneAndParseHeaders(headers: HeadersInit): Record { - if (Array.isArray(headers)) { - return headers.reduce( - (acc, headerPair) => { - acc[headerPair[0]] = headerPair[1]; - return acc; - }, - {} as Record, - ); - } else if ("has" in headers) { - const clonedHeaders: Record = {}; - (headers as Headers).forEach((value, key) => (clonedHeaders[key] = value)); - return clonedHeaders; - } else { - return Object.assign({}, headers); - } -} - -function createHeaders(config: Config): Record { +/** + * Creates a new Headers object from a {@link HeadersInit} and adds various + * properties to it, some from {@link Config}. + * + * @returns A new Headers object + */ +function getHeaders(config: Config, headersInit?: HeadersInit): Headers { const agentHeader = "X-Meilisearch-Client"; const packageAgent = `Meilisearch JavaScript (v${PACKAGE_VERSION})`; const contentType = "Content-Type"; const authorization = "Authorization"; - const headers = cloneAndParseHeaders(config.requestConfig?.headers ?? {}); + + const headers = new Headers(headersInit); // do not override if user provided the header - if (config.apiKey && !headers[authorization]) { - headers[authorization] = `Bearer ${config.apiKey}`; + if (config.apiKey && !headers.has(authorization)) { + headers.set(authorization, `Bearer ${config.apiKey}`); } - if (!headers[contentType]) { - headers["Content-Type"] = "application/json"; + if (!headers.has(contentType)) { + headers.set(contentType, "application/json"); } // Creates the custom user agent with information on the package used. - if (config.clientAgents && Array.isArray(config.clientAgents)) { + if (config.clientAgents !== undefined) { const clients = config.clientAgents.concat(packageAgent); - headers[agentHeader] = clients.join(" ; "); - } else if (config.clientAgents && !Array.isArray(config.clientAgents)) { - // If the header is defined but not an array - throw new MeiliSearchError( - `Meilisearch: The header "${agentHeader}" should be an array of string(s).\n`, - ); + headers.set(agentHeader, clients.join(" ; ")); } else { - headers[agentHeader] = packageAgent; + headers.set(agentHeader, packageAgent); } return headers; } -class HttpRequests { - headers: Record; - url: URL; - requestConfig?: Config["requestConfig"]; - httpClient?: Required["httpClient"]; - requestTimeout?: number; +// This could be a symbol, but Node.js 18 fetch doesn't support that yet +// and it might just go EOL before it ever does. +// https://github.com/nodejs/node/issues/49557 +const TIMEOUT_OBJECT = {}; + +/** + * Attach a timeout signal to a {@link RequestInit}, while preserving original + * signal functionality, if there is one. + * + * @remarks + * This could be a short few straight forward lines using {@link AbortSignal.any} + * and {@link AbortSignal.timeout}, but these aren't yet widely supported enough, + * nor polyfillable, at the time of writing. + * @returns A new function which starts the timeout, which then returns another + * function that clears the timeout + */ +function getTimeoutFn( + requestInit: RequestInit, + ms: number, +): () => (() => void) | void { + const { signal } = requestInit; + const ac = new AbortController(); + + if (signal != null) { + let acSignalFn: (() => void) | null = null; + + if (signal.aborted) { + ac.abort(signal.reason); + } else { + const fn = () => ac.abort(signal.reason); + + signal.addEventListener("abort", fn, { once: true }); + + acSignalFn = () => signal.removeEventListener("abort", fn); + ac.signal.addEventListener("abort", acSignalFn, { once: true }); + } + + return () => { + if (signal.aborted) { + return; + } + + const to = setTimeout(() => ac.abort(TIMEOUT_OBJECT), ms); + const fn = () => { + clearTimeout(to); + + if (acSignalFn !== null) { + ac.signal.removeEventListener("abort", acSignalFn); + } + }; + + signal.addEventListener("abort", fn, { once: true }); + + return () => { + signal.removeEventListener("abort", fn); + fn(); + }; + }; + } + + requestInit.signal = ac.signal; + + return () => { + const to = setTimeout(() => ac.abort(TIMEOUT_OBJECT), ms); + return () => clearTimeout(to); + }; +} + +/** Class used to perform HTTP requests. */ +export class HttpRequests { + #url: URL; + #requestInit: HttpRequestsRequestInit; + #customRequestFn?: Config["httpClient"]; + #requestTimeout?: Config["timeout"]; constructor(config: Config) { - this.headers = createHeaders(config); - this.requestConfig = config.requestConfig; - this.httpClient = config.httpClient; - this.requestTimeout = config.timeout; + const host = addTrailingSlash(addProtocolIfNotPresent(config.host)); try { - const host = constructHostURL(config.host); - this.url = new URL(host); - } catch { - throw new MeiliSearchError("The provided host is not valid."); + this.#url = new URL(host); + } catch (error) { + throw new MeiliSearchError("The provided host is not valid", { + cause: error, + }); } + + this.#requestInit = { + ...config.requestInit, + headers: getHeaders(config, config.requestInit?.headers), + }; + + this.#customRequestFn = config.httpClient; + this.#requestTimeout = config.timeout; + } + + /** + * Combines provided extra {@link RequestInit} headers, provided content type + * and class instance RequestInit headers, prioritizing them in this order. + * + * @returns A new Headers object or the main headers of this class if no + * headers are provided + */ + #getHeaders(extraHeaders?: HeadersInit, contentType?: string): Headers { + if (extraHeaders === undefined && contentType === undefined) { + return this.#requestInit.headers; + } + + const headers = new Headers(extraHeaders); + + if (contentType !== undefined && !headers.has("Content-Type")) { + headers.set("Content-Type", contentType); + } + + for (const [key, val] of this.#requestInit.headers) { + if (!headers.has(key)) { + headers.set(key, val); + } + } + + return headers; } - async request({ + /** + * Sends a request with {@link fetch} or a custom HTTP client, combining + * parameters and class properties. + * + * @returns A promise containing the response + */ + async #request({ + path, method, - url, params, + contentType, body, - config = {}, - }: { - method: string; - url: string; - params?: { [key: string]: any }; - body?: any; - config?: Record; - }) { - const constructURL = new URL(url, this.url); - if (params) { - const queryParams = new URLSearchParams(); - Object.keys(params) - .filter((x: string) => params[x] !== null) - .map((x: string) => queryParams.set(x, params[x])); - constructURL.search = queryParams.toString(); + extraRequestInit, + }: MainRequestOptions): Promise { + const url = new URL(path, this.#url); + if (params !== undefined) { + appendRecordToURLSearchParams(url.searchParams, params); } - // in case a custom content-type is provided - // do not stringify body - if (!config.headers?.["Content-Type"]) { - body = JSON.stringify(body); + const requestInit: RequestInit = { + method, + body: + contentType === undefined || typeof body !== "string" + ? JSON.stringify(body) + : body, + ...extraRequestInit, + ...this.#requestInit, + headers: this.#getHeaders(extraRequestInit?.headers, contentType), + }; + + const startTimeout = + this.#requestTimeout !== undefined + ? getTimeoutFn(requestInit, this.#requestTimeout) + : null; + + const getResponseAndHandleErrorAndTimeout = < + U extends ReturnType | typeof fetch>, + >( + responsePromise: U, + stopTimeout?: ReturnType>, + ) => + responsePromise + .catch((error: unknown) => { + throw new MeiliSearchRequestError( + url.toString(), + Object.is(error, TIMEOUT_OBJECT) + ? new Error(`request timed out after ${this.#requestTimeout}ms`, { + cause: requestInit, + }) + : error, + ); + }) + .finally(() => stopTimeout?.()) as U; + + const stopTimeout = startTimeout?.(); + + if (this.#customRequestFn !== undefined) { + const response = await getResponseAndHandleErrorAndTimeout( + this.#customRequestFn(url, requestInit), + stopTimeout, + ); + + // When using a custom HTTP client, the response should already be handled and ready to be returned + return response as T; } - const headers = { ...this.headers, ...config.headers }; - const responsePromise = this.fetchWithTimeout( - constructURL.toString(), - { - ...config, - ...this.requestConfig, - method, - body, - headers, - }, - this.requestTimeout, + const response = await getResponseAndHandleErrorAndTimeout( + fetch(url, requestInit), + stopTimeout, ); - const response = await responsePromise.catch((error: unknown) => { - throw new MeiliSearchRequestError(constructURL.toString(), error); - }); - - // When using a custom HTTP client, the response is returned to allow the user to parse/handle it as they see fit - if (this.httpClient !== undefined) { - return response; - } - const responseBody = await response.text(); const parsedResponse = responseBody === "" ? undefined : JSON.parse(responseBody); @@ -168,152 +268,31 @@ class HttpRequests { throw new MeiliSearchApiError(response, parsedResponse); } - return parsedResponse; - } - - async fetchWithTimeout( - url: string, - options: Record | RequestInit | undefined, - timeout: HttpRequests["requestTimeout"], - ): Promise { - return new Promise((resolve, reject) => { - const fetchFn = this.httpClient ? this.httpClient : fetch; - - const fetchPromise = fetchFn(url, options); - - const promises: Array> = [fetchPromise]; - - // TimeoutPromise will not run if undefined or zero - let timeoutId: ReturnType; - if (timeout) { - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error("Error: Request Timed Out")); - }, timeout); - }); - - promises.push(timeoutPromise); - } - - Promise.race(promises) - .then(resolve) - .catch(reject) - .finally(() => { - clearTimeout(timeoutId); - }); - }); + return parsedResponse as T; } - async get( - url: string, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - - async get( - url: string, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - - async get( - url: string, - params?: { [key: string]: any }, - config?: Record, - ): Promise { - return await this.request({ - method: "GET", - url, - params, - config, - }); + /** Request with GET. */ + get(options: RequestOptions): Promise { + return this.#request(options); } - async post( - url: string, - data?: T, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - - async post( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise { - return await this.request({ - method: "POST", - url, - body: data, - params, - config, - }); + /** Request with POST. */ + post(options: RequestOptions): Promise { + return this.#request({ ...options, method: "POST" }); } - async put( - url: string, - data?: T, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - - async put( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise { - return await this.request({ - method: "PUT", - url, - body: data, - params, - config, - }); + /** Request with PUT. */ + put(options: RequestOptions): Promise { + return this.#request({ ...options, method: "PUT" }); } - async patch( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise { - return await this.request({ - method: "PATCH", - url, - body: data, - params, - config, - }); + /** Request with PATCH. */ + patch(options: RequestOptions): Promise { + return this.#request({ ...options, method: "PATCH" }); } - async delete( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - async delete( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise; - async delete( - url: string, - data?: any, - params?: { [key: string]: any }, - config?: Record, - ): Promise { - return await this.request({ - method: "DELETE", - url, - body: data, - params, - config, - }); + /** Request with DELETE. */ + delete(options: RequestOptions): Promise { + return this.#request({ ...options, method: "DELETE" }); } } - -export { HttpRequests, toQueryParams }; diff --git a/src/indexes.ts b/src/indexes.ts index 5ad1b9ccb..804c73108 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -54,9 +54,10 @@ import type { SearchSimilarDocumentsParams, LocalizedAttributes, UpdateDocumentsByFunctionOptions, + EnqueuedTaskObject, + ExtraRequestInit, PrefixSearch, } from "./types.js"; -import { removeUndefinedFromObject } from "./utils.js"; import { HttpRequests } from "./http-requests.js"; import { Task, TaskClient } from "./task.js"; import { EnqueuedTask } from "./enqueued-task.js"; @@ -99,16 +100,13 @@ class Index = Record> { >( query?: string | null, options?: S, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise> { - const url = `indexes/${this.uid}/search`; - - return await this.httpRequest.post( - url, - removeUndefinedFromObject({ q: query, ...options }), - undefined, - config, - ); + return await this.httpRequest.post>({ + path: `indexes/${this.uid}/search`, + body: { q: query, ...options }, + extraRequestInit, + }); } /** @@ -125,10 +123,9 @@ class Index = Record> { >( query?: string | null, options?: S, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise> { - const url = `indexes/${this.uid}/search`; - + // TODO: Make this a type thing instead of a runtime thing const parseFilter = (filter?: Filter): string | undefined => { if (typeof filter === "string") return filter; else if (Array.isArray(filter)) @@ -151,11 +148,11 @@ class Index = Record> { attributesToSearchOn: options?.attributesToSearchOn?.join(","), }; - return await this.httpRequest.get>( - url, - removeUndefinedFromObject(getParams), - config, - ); + return await this.httpRequest.get>({ + path: `indexes/${this.uid}/search`, + params: getParams, + extraRequestInit, + }); } /** @@ -167,16 +164,13 @@ class Index = Record> { */ async searchForFacetValues( params: SearchForFacetValuesParams, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise { - const url = `indexes/${this.uid}/facet-search`; - - return await this.httpRequest.post( - url, - removeUndefinedFromObject(params), - undefined, - config, - ); + return await this.httpRequest.post({ + path: `indexes/${this.uid}/facet-search`, + body: params, + extraRequestInit, + }); } /** @@ -189,13 +183,10 @@ class Index = Record> { D extends Record = T, S extends SearchParams = SearchParams, >(params: SearchSimilarDocumentsParams): Promise> { - const url = `indexes/${this.uid}/similar`; - - return await this.httpRequest.post( - url, - removeUndefinedFromObject(params), - undefined, - ); + return await this.httpRequest.post>({ + path: `indexes/${this.uid}/similar`, + body: params, + }); } /// @@ -208,8 +199,9 @@ class Index = Record> { * @returns Promise containing index information */ async getRawInfo(): Promise { - const url = `indexes/${this.uid}`; - const res = await this.httpRequest.get(url); + const res = await this.httpRequest.get({ + path: `indexes/${this.uid}`, + }); this.primaryKey = res.primaryKey; this.updatedAt = new Date(res.updatedAt); this.createdAt = new Date(res.createdAt); @@ -249,9 +241,11 @@ class Index = Record> { options: IndexOptions = {}, config: Config, ): Promise { - const url = `indexes`; const req = new HttpRequests(config); - const task = await req.post(url, { ...options, uid }); + const task = await req.post({ + path: "indexes", + body: { ...options, uid }, + }); return new EnqueuedTask(task); } @@ -262,13 +256,13 @@ class Index = Record> { * @param data - Data to update * @returns Promise to the current Index object with updated information */ - async update(data: IndexOptions): Promise { - const url = `indexes/${this.uid}`; - const task = await this.httpRequest.patch(url, data); - - task.enqueuedAt = new Date(task.enqueuedAt); + async update(data?: IndexOptions): Promise { + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}`, + body: data, + }); - return task; + return new EnqueuedTask(task); } /** @@ -277,8 +271,9 @@ class Index = Record> { * @returns Promise which resolves when index is deleted successfully */ async delete(): Promise { - const url = `indexes/${this.uid}`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}`, + }); return new EnqueuedTask(task); } @@ -293,7 +288,7 @@ class Index = Record> { * @param parameters - Parameters to browse the tasks * @returns Promise containing all tasks */ - async getTasks(parameters: TasksQuery = {}): Promise { + async getTasks(parameters?: TasksQuery): Promise { return await this.tasks.getTasks({ ...parameters, indexUids: [this.uid] }); } @@ -351,8 +346,9 @@ class Index = Record> { * @returns Promise containing object with stats of the index */ async getStats(): Promise { - const url = `indexes/${this.uid}/stats`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/stats`, + }); } /// @@ -362,24 +358,22 @@ class Index = Record> { /** * Get documents of an index. * - * @param parameters - Parameters to browse the documents. Parameters can - * contain the `filter` field only available in Meilisearch v1.2 and newer + * @param params - Parameters to browse the documents. Parameters can contain + * the `filter` field only available in Meilisearch v1.2 and newer * @returns Promise containing the returned documents */ async getDocuments = T>( - parameters: DocumentsQuery = {}, + params?: DocumentsQuery, ): Promise> { - parameters = removeUndefinedFromObject(parameters); + const relativeBaseURL = `indexes/${this.uid}/documents`; // In case `filter` is provided, use `POST /documents/fetch` - if (parameters.filter !== undefined) { + if (params?.filter !== undefined) { try { - const url = `indexes/${this.uid}/documents/fetch`; - - return await this.httpRequest.post< - DocumentsQuery, - Promise> - >(url, parameters); + return await this.httpRequest.post>({ + path: `${relativeBaseURL}/fetch`, + body: params, + }); } catch (e) { if (e instanceof MeiliSearchRequestError) { e.message = versionErrorHintMessage(e.message, "getDocuments"); @@ -389,18 +383,11 @@ class Index = Record> { throw e; } - // Else use `GET /documents` method } else { - const url = `indexes/${this.uid}/documents`; - - // Transform fields to query parameter string format - const fields = Array.isArray(parameters?.fields) - ? { fields: parameters?.fields?.join(",") } - : {}; - - return await this.httpRequest.get>>(url, { - ...parameters, - ...fields, + // Else use `GET /documents` method + return await this.httpRequest.get>({ + path: relativeBaseURL, + params, }); } } @@ -416,8 +403,6 @@ class Index = Record> { documentId: string | number, parameters?: DocumentQuery, ): Promise { - const url = `indexes/${this.uid}/documents/${documentId}`; - const fields = (() => { if (Array.isArray(parameters?.fields)) { return parameters?.fields?.join(","); @@ -425,13 +410,10 @@ class Index = Record> { return undefined; })(); - return await this.httpRequest.get( - url, - removeUndefinedFromObject({ - ...parameters, - fields, - }), - ); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/documents/${documentId}`, + params: { ...parameters, fields }, + }); } /** @@ -445,8 +427,11 @@ class Index = Record> { documents: T[], options?: DocumentOptions, ): Promise { - const url = `indexes/${this.uid}/documents`; - const task = await this.httpRequest.post(url, documents, options); + const task = await this.httpRequest.post({ + path: `indexes/${this.uid}/documents`, + params: options, + body: documents, + }); return new EnqueuedTask(task); } @@ -466,12 +451,11 @@ class Index = Record> { contentType: ContentType, queryParams?: RawDocumentAdditionOptions, ): Promise { - const url = `indexes/${this.uid}/documents`; - - const task = await this.httpRequest.post(url, documents, queryParams, { - headers: { - "Content-Type": contentType, - }, + const task = await this.httpRequest.post({ + path: `indexes/${this.uid}/documents`, + body: documents, + params: queryParams, + contentType, }); return new EnqueuedTask(task); @@ -510,8 +494,11 @@ class Index = Record> { documents: Array>, options?: DocumentOptions, ): Promise { - const url = `indexes/${this.uid}/documents`; - const task = await this.httpRequest.put(url, documents, options); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/documents`, + params: options, + body: documents, + }); return new EnqueuedTask(task); } @@ -553,12 +540,11 @@ class Index = Record> { contentType: ContentType, queryParams?: RawDocumentAdditionOptions, ): Promise { - const url = `indexes/${this.uid}/documents`; - - const task = await this.httpRequest.put(url, documents, queryParams, { - headers: { - "Content-Type": contentType, - }, + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/documents`, + body: documents, + params: queryParams, + contentType, }); return new EnqueuedTask(task); @@ -571,12 +557,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async deleteDocument(documentId: string | number): Promise { - const url = `indexes/${this.uid}/documents/${documentId}`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/documents/${documentId}`, + }); - return task; + return new EnqueuedTask(task); } /** @@ -599,10 +584,12 @@ class Index = Record> { const endpoint = isDocumentsDeletionQuery ? "documents/delete" : "documents/delete-batch"; - const url = `indexes/${this.uid}/${endpoint}`; try { - const task = await this.httpRequest.post(url, params); + const task = await this.httpRequest.post({ + path: `indexes/${this.uid}/${endpoint}`, + body: params, + }); return new EnqueuedTask(task); } catch (e) { @@ -622,12 +609,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async deleteAllDocuments(): Promise { - const url = `indexes/${this.uid}/documents`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/documents`, + }); - return task; + return new EnqueuedTask(task); } /** @@ -645,8 +631,10 @@ class Index = Record> { async updateDocumentsByFunction( options: UpdateDocumentsByFunctionOptions, ): Promise { - const url = `indexes/${this.uid}/documents/edit`; - const task = await this.httpRequest.post(url, options); + const task = await this.httpRequest.post({ + path: `indexes/${this.uid}/documents/edit`, + body: options, + }); return new EnqueuedTask(task); } @@ -661,8 +649,9 @@ class Index = Record> { * @returns Promise containing Settings object */ async getSettings(): Promise { - const url = `indexes/${this.uid}/settings`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings`, + }); } /** @@ -672,12 +661,12 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateSettings(settings: Settings): Promise { - const url = `indexes/${this.uid}/settings`; - const task = await this.httpRequest.patch(url, settings); - - task.enqueued = new Date(task.enqueuedAt); + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}/settings`, + body: settings, + }); - return task; + return new EnqueuedTask(task); } /** @@ -686,12 +675,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSettings(): Promise { - const url = `indexes/${this.uid}/settings`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -704,8 +692,9 @@ class Index = Record> { * @returns Promise containing object of pagination settings */ async getPagination(): Promise { - const url = `indexes/${this.uid}/settings/pagination`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/pagination`, + }); } /** @@ -717,8 +706,10 @@ class Index = Record> { async updatePagination( pagination: PaginationSettings, ): Promise { - const url = `indexes/${this.uid}/settings/pagination`; - const task = await this.httpRequest.patch(url, pagination); + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}/settings/pagination`, + body: pagination, + }); return new EnqueuedTask(task); } @@ -729,8 +720,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetPagination(): Promise { - const url = `indexes/${this.uid}/settings/pagination`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/pagination`, + }); return new EnqueuedTask(task); } @@ -742,11 +734,12 @@ class Index = Record> { /** * Get the list of all synonyms * - * @returns Promise containing object of synonym mappings + * @returns Promise containing record of synonym mappings */ - async getSynonyms(): Promise { - const url = `indexes/${this.uid}/settings/synonyms`; - return await this.httpRequest.get(url); + async getSynonyms(): Promise> { + return await this.httpRequest.get>({ + path: `indexes/${this.uid}/settings/synonyms`, + }); } /** @@ -756,8 +749,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateSynonyms(synonyms: Synonyms): Promise { - const url = `indexes/${this.uid}/settings/synonyms`; - const task = await this.httpRequest.put(url, synonyms); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/synonyms`, + body: synonyms, + }); return new EnqueuedTask(task); } @@ -768,12 +763,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSynonyms(): Promise { - const url = `indexes/${this.uid}/settings/synonyms`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/synonyms`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -786,8 +780,9 @@ class Index = Record> { * @returns Promise containing array of stop-words */ async getStopWords(): Promise { - const url = `indexes/${this.uid}/settings/stop-words`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/stop-words`, + }); } /** @@ -797,8 +792,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateStopWords(stopWords: StopWords): Promise { - const url = `indexes/${this.uid}/settings/stop-words`; - const task = await this.httpRequest.put(url, stopWords); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/stop-words`, + body: stopWords, + }); return new EnqueuedTask(task); } @@ -809,12 +806,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetStopWords(): Promise { - const url = `indexes/${this.uid}/settings/stop-words`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/stop-words`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -827,8 +823,9 @@ class Index = Record> { * @returns Promise containing array of ranking-rules */ async getRankingRules(): Promise { - const url = `indexes/${this.uid}/settings/ranking-rules`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/ranking-rules`, + }); } /** @@ -839,8 +836,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateRankingRules(rankingRules: RankingRules): Promise { - const url = `indexes/${this.uid}/settings/ranking-rules`; - const task = await this.httpRequest.put(url, rankingRules); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/ranking-rules`, + body: rankingRules, + }); return new EnqueuedTask(task); } @@ -851,12 +850,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetRankingRules(): Promise { - const url = `indexes/${this.uid}/settings/ranking-rules`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/ranking-rules`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -868,9 +866,10 @@ class Index = Record> { * * @returns Promise containing the distinct-attribute of the index */ - async getDistinctAttribute(): Promise { - const url = `indexes/${this.uid}/settings/distinct-attribute`; - return await this.httpRequest.get(url); + async getDistinctAttribute(): Promise { + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/distinct-attribute`, + }); } /** @@ -882,8 +881,10 @@ class Index = Record> { async updateDistinctAttribute( distinctAttribute: DistinctAttribute, ): Promise { - const url = `indexes/${this.uid}/settings/distinct-attribute`; - const task = await this.httpRequest.put(url, distinctAttribute); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/distinct-attribute`, + body: distinctAttribute, + }); return new EnqueuedTask(task); } @@ -894,12 +895,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetDistinctAttribute(): Promise { - const url = `indexes/${this.uid}/settings/distinct-attribute`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/distinct-attribute`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -912,8 +912,9 @@ class Index = Record> { * @returns Promise containing an array of filterable-attributes */ async getFilterableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/filterable-attributes`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/filterable-attributes`, + }); } /** @@ -926,8 +927,10 @@ class Index = Record> { async updateFilterableAttributes( filterableAttributes: FilterableAttributes, ): Promise { - const url = `indexes/${this.uid}/settings/filterable-attributes`; - const task = await this.httpRequest.put(url, filterableAttributes); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/filterable-attributes`, + body: filterableAttributes, + }); return new EnqueuedTask(task); } @@ -938,12 +941,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetFilterableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/filterable-attributes`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/filterable-attributes`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -956,8 +958,9 @@ class Index = Record> { * @returns Promise containing array of sortable-attributes */ async getSortableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/sortable-attributes`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/sortable-attributes`, + }); } /** @@ -970,8 +973,10 @@ class Index = Record> { async updateSortableAttributes( sortableAttributes: SortableAttributes, ): Promise { - const url = `indexes/${this.uid}/settings/sortable-attributes`; - const task = await this.httpRequest.put(url, sortableAttributes); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/sortable-attributes`, + body: sortableAttributes, + }); return new EnqueuedTask(task); } @@ -982,12 +987,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSortableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/sortable-attributes`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/sortable-attributes`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1000,8 +1004,9 @@ class Index = Record> { * @returns Promise containing array of searchable-attributes */ async getSearchableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/searchable-attributes`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/searchable-attributes`, + }); } /** @@ -1014,8 +1019,10 @@ class Index = Record> { async updateSearchableAttributes( searchableAttributes: SearchableAttributes, ): Promise { - const url = `indexes/${this.uid}/settings/searchable-attributes`; - const task = await this.httpRequest.put(url, searchableAttributes); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/searchable-attributes`, + body: searchableAttributes, + }); return new EnqueuedTask(task); } @@ -1026,12 +1033,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSearchableAttributes(): Promise { - const url = `indexes/${this.uid}/settings/searchable-attributes`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/searchable-attributes`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1044,8 +1050,9 @@ class Index = Record> { * @returns Promise containing array of displayed-attributes */ async getDisplayedAttributes(): Promise { - const url = `indexes/${this.uid}/settings/displayed-attributes`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/displayed-attributes`, + }); } /** @@ -1058,8 +1065,10 @@ class Index = Record> { async updateDisplayedAttributes( displayedAttributes: DisplayedAttributes, ): Promise { - const url = `indexes/${this.uid}/settings/displayed-attributes`; - const task = await this.httpRequest.put(url, displayedAttributes); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/displayed-attributes`, + body: displayedAttributes, + }); return new EnqueuedTask(task); } @@ -1070,12 +1079,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetDisplayedAttributes(): Promise { - const url = `indexes/${this.uid}/settings/displayed-attributes`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/displayed-attributes`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1088,8 +1096,9 @@ class Index = Record> { * @returns Promise containing the typo tolerance settings. */ async getTypoTolerance(): Promise { - const url = `indexes/${this.uid}/settings/typo-tolerance`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/typo-tolerance`, + }); } /** @@ -1102,12 +1111,12 @@ class Index = Record> { async updateTypoTolerance( typoTolerance: TypoTolerance, ): Promise { - const url = `indexes/${this.uid}/settings/typo-tolerance`; - const task = await this.httpRequest.patch(url, typoTolerance); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}/settings/typo-tolerance`, + body: typoTolerance, + }); - return task; + return new EnqueuedTask(task); } /** @@ -1116,12 +1125,11 @@ class Index = Record> { * @returns Promise containing object of the enqueued update */ async resetTypoTolerance(): Promise { - const url = `indexes/${this.uid}/settings/typo-tolerance`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/typo-tolerance`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1134,8 +1142,9 @@ class Index = Record> { * @returns Promise containing object of faceting index settings */ async getFaceting(): Promise { - const url = `indexes/${this.uid}/settings/faceting`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/faceting`, + }); } /** @@ -1145,8 +1154,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateFaceting(faceting: Faceting): Promise { - const url = `indexes/${this.uid}/settings/faceting`; - const task = await this.httpRequest.patch(url, faceting); + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}/settings/faceting`, + body: faceting, + }); return new EnqueuedTask(task); } @@ -1157,8 +1168,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetFaceting(): Promise { - const url = `indexes/${this.uid}/settings/faceting`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/faceting`, + }); return new EnqueuedTask(task); } @@ -1173,8 +1185,9 @@ class Index = Record> { * @returns Promise containing array of separator tokens */ async getSeparatorTokens(): Promise { - const url = `indexes/${this.uid}/settings/separator-tokens`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/separator-tokens`, + }); } /** @@ -1186,8 +1199,10 @@ class Index = Record> { async updateSeparatorTokens( separatorTokens: SeparatorTokens, ): Promise { - const url = `indexes/${this.uid}/settings/separator-tokens`; - const task = await this.httpRequest.put(url, separatorTokens); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/separator-tokens`, + body: separatorTokens, + }); return new EnqueuedTask(task); } @@ -1198,12 +1213,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSeparatorTokens(): Promise { - const url = `indexes/${this.uid}/settings/separator-tokens`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/separator-tokens`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1216,8 +1230,9 @@ class Index = Record> { * @returns Promise containing array of non-separator tokens */ async getNonSeparatorTokens(): Promise { - const url = `indexes/${this.uid}/settings/non-separator-tokens`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/non-separator-tokens`, + }); } /** @@ -1229,8 +1244,10 @@ class Index = Record> { async updateNonSeparatorTokens( nonSeparatorTokens: NonSeparatorTokens, ): Promise { - const url = `indexes/${this.uid}/settings/non-separator-tokens`; - const task = await this.httpRequest.put(url, nonSeparatorTokens); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/non-separator-tokens`, + body: nonSeparatorTokens, + }); return new EnqueuedTask(task); } @@ -1241,12 +1258,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetNonSeparatorTokens(): Promise { - const url = `indexes/${this.uid}/settings/non-separator-tokens`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/non-separator-tokens`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1259,8 +1275,9 @@ class Index = Record> { * @returns Promise containing the dictionary settings */ async getDictionary(): Promise { - const url = `indexes/${this.uid}/settings/dictionary`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/dictionary`, + }); } /** @@ -1270,8 +1287,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask or null */ async updateDictionary(dictionary: Dictionary): Promise { - const url = `indexes/${this.uid}/settings/dictionary`; - const task = await this.httpRequest.put(url, dictionary); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/dictionary`, + body: dictionary, + }); return new EnqueuedTask(task); } @@ -1282,12 +1301,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetDictionary(): Promise { - const url = `indexes/${this.uid}/settings/dictionary`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/dictionary`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1300,8 +1318,9 @@ class Index = Record> { * @returns Promise containing the proximity precision settings */ async getProximityPrecision(): Promise { - const url = `indexes/${this.uid}/settings/proximity-precision`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/proximity-precision`, + }); } /** @@ -1314,8 +1333,10 @@ class Index = Record> { async updateProximityPrecision( proximityPrecision: ProximityPrecision, ): Promise { - const url = `indexes/${this.uid}/settings/proximity-precision`; - const task = await this.httpRequest.put(url, proximityPrecision); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/proximity-precision`, + body: proximityPrecision, + }); return new EnqueuedTask(task); } @@ -1326,12 +1347,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetProximityPrecision(): Promise { - const url = `indexes/${this.uid}/settings/proximity-precision`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/proximity-precision`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1344,8 +1364,9 @@ class Index = Record> { * @returns Promise containing the embedders settings */ async getEmbedders(): Promise { - const url = `indexes/${this.uid}/settings/embedders`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/embedders`, + }); } /** @@ -1355,8 +1376,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask or null */ async updateEmbedders(embedders: Embedders): Promise { - const url = `indexes/${this.uid}/settings/embedders`; - const task = await this.httpRequest.patch(url, embedders); + const task = await this.httpRequest.patch({ + path: `indexes/${this.uid}/settings/embedders`, + body: embedders, + }); return new EnqueuedTask(task); } @@ -1367,12 +1390,11 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetEmbedders(): Promise { - const url = `indexes/${this.uid}/settings/embedders`; - const task = await this.httpRequest.delete(url); - - task.enqueuedAt = new Date(task.enqueuedAt); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/embedders`, + }); - return task; + return new EnqueuedTask(task); } /// @@ -1385,8 +1407,9 @@ class Index = Record> { * @returns Promise containing object of SearchCutoffMs settings */ async getSearchCutoffMs(): Promise { - const url = `indexes/${this.uid}/settings/search-cutoff-ms`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/search-cutoff-ms`, + }); } /** @@ -1398,8 +1421,10 @@ class Index = Record> { async updateSearchCutoffMs( searchCutoffMs: SearchCutoffMs, ): Promise { - const url = `indexes/${this.uid}/settings/search-cutoff-ms`; - const task = await this.httpRequest.put(url, searchCutoffMs); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/search-cutoff-ms`, + body: searchCutoffMs, + }); return new EnqueuedTask(task); } @@ -1410,8 +1435,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetSearchCutoffMs(): Promise { - const url = `indexes/${this.uid}/settings/search-cutoff-ms`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/search-cutoff-ms`, + }); return new EnqueuedTask(task); } @@ -1426,8 +1452,9 @@ class Index = Record> { * @returns Promise containing object of localized attributes settings */ async getLocalizedAttributes(): Promise { - const url = `indexes/${this.uid}/settings/localized-attributes`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/localized-attributes`, + }); } /** @@ -1439,8 +1466,10 @@ class Index = Record> { async updateLocalizedAttributes( localizedAttributes: LocalizedAttributes, ): Promise { - const url = `indexes/${this.uid}/settings/localized-attributes`; - const task = await this.httpRequest.put(url, localizedAttributes); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/localized-attributes`, + body: localizedAttributes, + }); return new EnqueuedTask(task); } @@ -1451,8 +1480,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetLocalizedAttributes(): Promise { - const url = `indexes/${this.uid}/settings/localized-attributes`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/localized-attributes`, + }); return new EnqueuedTask(task); } @@ -1467,8 +1497,9 @@ class Index = Record> { * @returns Promise containing object of facet search settings */ async getFacetSearch(): Promise { - const url = `indexes/${this.uid}/settings/facet-search`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/facet-search`, + }); } /** @@ -1478,8 +1509,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updateFacetSearch(facetSearch: boolean): Promise { - const url = `indexes/${this.uid}/settings/facet-search`; - const task = await this.httpRequest.put(url, facetSearch); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/facet-search`, + body: facetSearch, + }); return new EnqueuedTask(task); } @@ -1489,8 +1522,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetFacetSearch(): Promise { - const url = `indexes/${this.uid}/settings/facet-search`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/facet-search`, + }); return new EnqueuedTask(task); } @@ -1504,8 +1538,9 @@ class Index = Record> { * @returns Promise containing object of prefix search settings */ async getPrefixSearch(): Promise { - const url = `indexes/${this.uid}/settings/prefix-search`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `indexes/${this.uid}/settings/prefix-search`, + }); } /** @@ -1515,8 +1550,10 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async updatePrefixSearch(prefixSearch: PrefixSearch): Promise { - const url = `indexes/${this.uid}/settings/prefix-search`; - const task = await this.httpRequest.put(url, prefixSearch); + const task = await this.httpRequest.put({ + path: `indexes/${this.uid}/settings/prefix-search`, + body: prefixSearch, + }); return new EnqueuedTask(task); } @@ -1526,8 +1563,9 @@ class Index = Record> { * @returns Promise containing an EnqueuedTask */ async resetPrefixSearch(): Promise { - const url = `indexes/${this.uid}/settings/prefix-search`; - const task = await this.httpRequest.delete(url); + const task = await this.httpRequest.delete({ + path: `indexes/${this.uid}/settings/prefix-search`, + }); return new EnqueuedTask(task); } } diff --git a/src/meilisearch.ts b/src/meilisearch.ts index b6899df9e..31e7429a3 100644 --- a/src/meilisearch.ts +++ b/src/meilisearch.ts @@ -15,27 +15,23 @@ import type { Health, Stats, Version, - TasksQuery, - WaitOptions, KeyUpdate, IndexesQuery, IndexesResults, KeysQuery, KeysResults, - TasksResults, EnqueuedTaskObject, SwapIndexesParams, - CancelTasksQuery, - DeleteTasksQuery, MultiSearchParams, FederatedMultiSearchParams, + ExtraRequestInit, BatchesResults, BatchesQuery, MultiSearchResponseOrSearchResponse, } from "./types.js"; import { ErrorStatusCode } from "./types.js"; import { HttpRequests } from "./http-requests.js"; -import { TaskClient, type Task } from "./task.js"; +import { TaskClient } from "./task.js"; import { EnqueuedTask } from "./enqueued-task.js"; import { type Batch, BatchClient } from "./batch.js"; @@ -100,7 +96,7 @@ export class MeiliSearch { * @returns Promise returning array of raw index information */ async getIndexes( - parameters: IndexesQuery = {}, + parameters?: IndexesQuery, ): Promise> { const rawIndexes = await this.getRawIndexes(parameters); const indexes: Index[] = rawIndexes.results.map( @@ -116,13 +112,12 @@ export class MeiliSearch { * @returns Promise returning array of raw index information */ async getRawIndexes( - parameters: IndexesQuery = {}, + parameters?: IndexesQuery, ): Promise> { - const url = `indexes`; - return await this.httpRequest.get>( - url, - parameters, - ); + return await this.httpRequest.get>({ + path: "indexes", + params: parameters, + }); } /** @@ -134,7 +129,7 @@ export class MeiliSearch { */ async createIndex( uid: string, - options: IndexOptions = {}, + options?: IndexOptions, ): Promise { return await Index.create(uid, options, this.config); } @@ -148,7 +143,7 @@ export class MeiliSearch { */ async updateIndex( uid: string, - options: IndexOptions = {}, + options?: IndexOptions, ): Promise { return await new Index(this.config, uid).update(options); } @@ -190,7 +185,12 @@ export class MeiliSearch { */ async swapIndexes(params: SwapIndexesParams): Promise { const url = "/swap-indexes"; - return await this.httpRequest.post(url, params); + const taks = await this.httpRequest.post({ + path: url, + body: params, + }); + + return new EnqueuedTask(taks); } /// @@ -215,19 +215,23 @@ export class MeiliSearch { * ``` * * @param queries - Search queries - * @param config - Additional request configuration options + * @param extraRequestInit - Additional request configuration options * @returns Promise containing the search responses */ async multiSearch< T1 extends MultiSearchParams | FederatedMultiSearchParams, - T2 extends Record = Record, + T2 extends Record = Record, >( queries: T1, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise> { - const url = `multi-search`; - - return await this.httpRequest.post(url, queries, undefined, config); + return await this.httpRequest.post< + MultiSearchResponseOrSearchResponse + >({ + path: "multi-search", + body: queries, + extraRequestInit, + }); } /// @@ -240,8 +244,10 @@ export class MeiliSearch { * @param parameters - Parameters to browse the tasks * @returns Promise returning all tasks */ - async getTasks(parameters: TasksQuery = {}): Promise { - return await this.tasks.getTasks(parameters); + async getTasks( + ...params: Parameters + ): ReturnType { + return await this.tasks.getTasks(...params); } /** @@ -250,8 +256,10 @@ export class MeiliSearch { * @param taskUid - Task identifier * @returns Promise returning a task */ - async getTask(taskUid: number): Promise { - return await this.tasks.getTask(taskUid); + async getTask( + ...params: Parameters + ): ReturnType { + return await this.tasks.getTask(...params); } /** @@ -262,13 +270,9 @@ export class MeiliSearch { * @returns Promise returning an array of tasks */ async waitForTasks( - taskUids: number[], - { timeOutMs = 5000, intervalMs = 50 }: WaitOptions = {}, - ): Promise { - return await this.tasks.waitForTasks(taskUids, { - timeOutMs, - intervalMs, - }); + ...params: Parameters + ): ReturnType { + return await this.tasks.waitForTasks(...params); } /** @@ -279,13 +283,9 @@ export class MeiliSearch { * @returns Promise returning an array of tasks */ async waitForTask( - taskUid: number, - { timeOutMs = 5000, intervalMs = 50 }: WaitOptions = {}, - ): Promise { - return await this.tasks.waitForTask(taskUid, { - timeOutMs, - intervalMs, - }); + ...params: Parameters + ): ReturnType { + return await this.tasks.waitForTask(...params); } /** @@ -294,8 +294,10 @@ export class MeiliSearch { * @param parameters - Parameters to filter the tasks. * @returns Promise containing an EnqueuedTask */ - async cancelTasks(parameters: CancelTasksQuery): Promise { - return await this.tasks.cancelTasks(parameters); + async cancelTasks( + ...params: Parameters + ): ReturnType { + return await this.tasks.cancelTasks(...params); } /** @@ -304,8 +306,10 @@ export class MeiliSearch { * @param parameters - Parameters to filter the tasks. * @returns Promise containing an EnqueuedTask */ - async deleteTasks(parameters: DeleteTasksQuery = {}): Promise { - return await this.tasks.deleteTasks(parameters); + async deleteTasks( + ...params: Parameters + ): ReturnType { + return await this.tasks.deleteTasks(...params); } /** @@ -338,9 +342,11 @@ export class MeiliSearch { * @param parameters - Parameters to browse the indexes * @returns Promise returning an object with keys */ - async getKeys(parameters: KeysQuery = {}): Promise { - const url = `keys`; - const keys = await this.httpRequest.get(url, parameters); + async getKeys(parameters?: KeysQuery): Promise { + const keys = await this.httpRequest.get({ + path: "keys", + params: parameters, + }); keys.results = keys.results.map((key) => ({ ...key, @@ -358,8 +364,9 @@ export class MeiliSearch { * @returns Promise returning a key */ async getKey(keyOrUid: string): Promise { - const url = `keys/${keyOrUid}`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ + path: `keys/${keyOrUid}`, + }); } /** @@ -369,8 +376,10 @@ export class MeiliSearch { * @returns Promise returning a key */ async createKey(options: KeyCreation): Promise { - const url = `keys`; - return await this.httpRequest.post(url, options); + return await this.httpRequest.post({ + path: "keys", + body: options, + }); } /** @@ -381,8 +390,10 @@ export class MeiliSearch { * @returns Promise returning a key */ async updateKey(keyOrUid: string, options: KeyUpdate): Promise { - const url = `keys/${keyOrUid}`; - return await this.httpRequest.patch(url, options); + return await this.httpRequest.patch({ + path: `keys/${keyOrUid}`, + body: options, + }); } /** @@ -392,8 +403,7 @@ export class MeiliSearch { * @returns */ async deleteKey(keyOrUid: string): Promise { - const url = `keys/${keyOrUid}`; - return await this.httpRequest.delete(url); + await this.httpRequest.delete({ path: `keys/${keyOrUid}` }); } /// @@ -406,8 +416,7 @@ export class MeiliSearch { * @returns Promise returning an object with health details */ async health(): Promise { - const url = `health`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ path: "health" }); } /** @@ -417,9 +426,8 @@ export class MeiliSearch { */ async isHealthy(): Promise { try { - const url = `health`; - await this.httpRequest.get(url); - return true; + const { status } = await this.health(); + return status === "available"; } catch { return false; } @@ -435,8 +443,7 @@ export class MeiliSearch { * @returns Promise returning object of all the stats */ async getStats(): Promise { - const url = `stats`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ path: "stats" }); } /// @@ -449,8 +456,7 @@ export class MeiliSearch { * @returns Promise returning object with version details */ async getVersion(): Promise { - const url = `version`; - return await this.httpRequest.get(url); + return await this.httpRequest.get({ path: "version" }); } /// @@ -463,10 +469,10 @@ export class MeiliSearch { * @returns Promise returning object of the enqueued task */ async createDump(): Promise { - const url = `dumps`; - const task = await this.httpRequest.post( - url, - ); + const task = await this.httpRequest.post({ + path: "dumps", + }); + return new EnqueuedTask(task); } @@ -480,10 +486,9 @@ export class MeiliSearch { * @returns Promise returning object of the enqueued task */ async createSnapshot(): Promise { - const url = `snapshots`; - const task = await this.httpRequest.post( - url, - ); + const task = await this.httpRequest.post({ + path: "snapshots", + }); return new EnqueuedTask(task); } diff --git a/src/task.ts b/src/task.ts index 1c668f4a7..912e1795a 100644 --- a/src/task.ts +++ b/src/task.ts @@ -8,9 +8,10 @@ import type { CancelTasksQuery, TasksResultsObject, DeleteTasksQuery, + EnqueuedTaskObject, } from "./types.js"; import { TaskStatus } from "./types.js"; -import { HttpRequests, toQueryParams } from "./http-requests.js"; +import { HttpRequests } from "./http-requests.js"; import { sleep } from "./utils.js"; import { EnqueuedTask } from "./enqueued-task.js"; @@ -59,24 +60,23 @@ class TaskClient { * @returns */ async getTask(uid: number): Promise { - const url = `tasks/${uid}`; - const taskItem = await this.httpRequest.get(url); + const taskItem = await this.httpRequest.get({ + path: `tasks/${uid}`, + }); return new Task(taskItem); } /** * Get tasks * - * @param parameters - Parameters to browse the tasks + * @param params - Parameters to browse the tasks * @returns Promise containing all tasks */ - async getTasks(parameters: TasksQuery = {}): Promise { - const url = `tasks`; - - const tasks = await this.httpRequest.get>( - url, - toQueryParams(parameters), - ); + async getTasks(params?: TasksQuery): Promise { + const tasks = await this.httpRequest.get({ + path: "tasks", + params, + }); return { ...tasks, @@ -139,17 +139,14 @@ class TaskClient { /** * Cancel a list of enqueued or processing tasks. * - * @param parameters - Parameters to filter the tasks. + * @param params - Parameters to filter the tasks. * @returns Promise containing an EnqueuedTask */ - async cancelTasks(parameters: CancelTasksQuery = {}): Promise { - const url = `tasks/cancel`; - - const task = await this.httpRequest.post( - url, - {}, - toQueryParams(parameters), - ); + async cancelTasks(params?: CancelTasksQuery): Promise { + const task = await this.httpRequest.post({ + path: "tasks/cancel", + params, + }); return new EnqueuedTask(task); } @@ -157,17 +154,14 @@ class TaskClient { /** * Delete a list tasks. * - * @param parameters - Parameters to filter the tasks. + * @param params - Parameters to filter the tasks. * @returns Promise containing an EnqueuedTask */ - async deleteTasks(parameters: DeleteTasksQuery = {}): Promise { - const url = `tasks`; - - const task = await this.httpRequest.delete( - url, - {}, - toQueryParams(parameters), - ); + async deleteTasks(params?: DeleteTasksQuery): Promise { + const task = await this.httpRequest.delete({ + path: "tasks", + params, + }); return new EnqueuedTask(task); } } diff --git a/src/types.ts b/src/types.ts index 2a73fead3..6eae0fd1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,15 +7,110 @@ import { Task } from "./task.js"; import { Batch } from "./batch.js"; +/** + * Shape of allowed record object that can be appended to a + * {@link URLSearchParams}. + */ +export type URLSearchParamsRecord = Record< + string, + | string + | string[] + | Array + | number + | number[] + | boolean + | Date + | null + | undefined +>; + +/** + * {@link RequestInit} without {@link RequestInit.body} and + * {@link RequestInit.method} properties. + */ +export type ExtraRequestInit = Omit; + +/** Same as {@link ExtraRequestInit} but without {@link ExtraRequestInit.signal}. */ +export type BaseRequestInit = Omit; + +/** + * Same as {@link BaseRequestInit} but with its headers property forced as a + * {@link Headers} object. + */ +export type HttpRequestsRequestInit = Omit & { + headers: Headers; +}; + +/** Main configuration object for the meilisearch client. */ export type Config = { + /** + * The base URL for reaching a meilisearch instance. + * + * @remarks + * Protocol and trailing slash can be omitted. + */ host: string; + /** + * API key for interacting with a meilisearch instance. + * + * @see {@link https://www.meilisearch.com/docs/learn/security/basic_security} + */ apiKey?: string; + /** + * Custom strings that will be concatted to the "X-Meilisearch-Client" header + * on each request. + */ clientAgents?: string[]; - requestConfig?: Partial>; - httpClient?: (input: string, init?: RequestInit) => Promise; + /** Base request options that may override the default ones. */ + requestInit?: BaseRequestInit; + /** + * Custom function that can be provided in place of {@link fetch}. + * + * @remarks + * API response errors will have to be handled manually with this as well. + * @deprecated This will be removed in a future version. See + * {@link https://github.com/meilisearch/meilisearch-js/issues/1824 | issue}. + */ + httpClient?: (...args: Parameters) => Promise; + /** Timeout in milliseconds for each HTTP request. */ timeout?: number; }; +/** Main options of a request. */ +export type MainRequestOptions = { + /** The path or subpath of the URL to make a request to. */ + path: string; + /** The REST method of the request. */ + method?: string; + /** The search parameters of the URL. */ + params?: URLSearchParamsRecord; + /** + * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type | Content-Type} + * passed to request {@link Headers}. + */ + contentType?: string; + /** + * The body of the request. + * + * @remarks + * This only really supports string for now (any other type gets stringified) + * but it could support more in the future. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/RequestInit#body} + */ + body?: string | boolean | number | object | null; + /** + * An extra, more limited {@link RequestInit}, that may override some of the + * options. + */ + extraRequestInit?: ExtraRequestInit; +}; + +/** + * {@link MainRequestOptions} without {@link MainRequestOptions.method}, for + * method functions. + */ +export type RequestOptions = Omit; + /// /// Resources /// @@ -385,9 +480,7 @@ export type SortableAttributes = string[] | null; export type DisplayedAttributes = string[] | null; export type RankingRules = string[] | null; export type StopWords = string[] | null; -export type Synonyms = { - [field: string]: string[]; -} | null; +export type Synonyms = Record | null; export type TypoTolerance = { enabled?: boolean | null; disableOnAttributes?: string[] | null; @@ -577,9 +670,9 @@ export type TasksQuery = { reverse?: boolean; }; -export type CancelTasksQuery = Omit & {}; +export type CancelTasksQuery = Omit; -export type DeleteTasksQuery = Omit & {}; +export type DeleteTasksQuery = Omit; export type EnqueuedTaskObject = { taskUid: number; @@ -1229,7 +1322,7 @@ export type TokenIndexRules = { filter?: Filter }; * * @remarks * Not well documented. - * @see {@link https://github.com/meilisearch/meilisearch/blob/b21d7aedf9096539041362d438e973a18170f3fc/crates/meilisearch-auth/src/lib.rs#L271-L277 | GitHub source code} + * @see `meilisearch_auth::SearchRules` at {@link https://github.com/meilisearch/meilisearch} */ export type TokenSearchRules = | Record diff --git a/src/utils.ts b/src/utils.ts index b3a537d5f..e748e1f10 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,15 +1,3 @@ -/** Removes undefined entries from object */ -function removeUndefinedFromObject(obj: Record): object { - return Object.entries(obj).reduce( - (acc, curEntry) => { - const [key, val] = curEntry; - if (val !== undefined) acc[key] = val; - return acc; - }, - {} as Record, - ); -} - async function sleep(ms: number): Promise { return await new Promise((resolve) => setTimeout(resolve, ms)); } @@ -28,9 +16,4 @@ function addTrailingSlash(url: string): string { return url; } -export { - sleep, - removeUndefinedFromObject, - addProtocolIfNotPresent, - addTrailingSlash, -}; +export { sleep, addProtocolIfNotPresent, addTrailingSlash }; diff --git a/tests/client.test.ts b/tests/client.test.ts index 24b69531a..b745fed53 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,4 +1,14 @@ -import { afterAll, expect, test, describe, beforeEach } from "vitest"; +import { + afterAll, + expect, + test, + describe, + beforeEach, + vi, + type MockInstance, + beforeAll, + assert, +} from "vitest"; import type { Health, Version, Stats } from "../src/index.js"; import { ErrorStatusCode, TaskTypes } from "../src/index.js"; import { PACKAGE_VERSION } from "../src/package-version.js"; @@ -51,50 +61,89 @@ describe.each([ expect(health).toBe(true); }); - test(`${permission} key: Create client with custom headers (object)`, async () => { - const key = await getKey(permission); - const client = new MeiliSearch({ - ...config, - apiKey: key, - requestConfig: { - headers: { - "Hello-There!": "General Kenobi", + describe("Header tests", () => { + let fetchSpy: MockInstance; + + beforeAll(() => { + fetchSpy = vi.spyOn(globalThis, "fetch"); + }); + + afterAll(() => fetchSpy.mockRestore()); + + test(`${permission} key: Create client with custom headers (object)`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + requestInit: { + headers: { + "Hello-There!": "General Kenobi", + }, }, - }, + }); + + await client.multiSearch( + { queries: [] }, + { headers: { "Jane-Doe": "John Doe" } }, + ); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + + const headers = requestInit!.headers! as Headers; + + assert.strictEqual(headers.get("Hello-There!"), "General Kenobi"); + assert.strictEqual(headers.get("Jane-Doe"), "John Doe"); }); - expect(client.httpRequest.headers["Hello-There!"]).toBe("General Kenobi"); - const health = await client.isHealthy(); - expect(health).toBe(true); - }); - test(`${permission} key: Create client with custom headers (array)`, async () => { - const key = await getKey(permission); - const client = new MeiliSearch({ - ...config, - apiKey: key, - requestConfig: { - headers: [["Hello-There!", "General Kenobi"]], - }, + test(`${permission} key: Create client with custom headers (array)`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + requestInit: { + headers: [["Hello-There!", "General Kenobi"]], + }, + }); + + assert.isTrue(await client.isHealthy()); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + assert.strictEqual( + (requestInit!.headers! as Headers).get("Hello-There!"), + "General Kenobi", + ); }); - expect(client.httpRequest.headers["Hello-There!"]).toBe("General Kenobi"); - const health = await client.isHealthy(); - expect(health).toBe(true); - }); - test(`${permission} key: Create client with custom headers (Headers)`, async () => { - const key = await getKey(permission); - const headers = new Headers(); - headers.append("Hello-There!", "General Kenobi"); - const client = new MeiliSearch({ - ...config, - apiKey: key, - requestConfig: { - headers, - }, + test(`${permission} key: Create client with custom headers (Headers)`, async () => { + const key = await getKey(permission); + const headers = new Headers(); + headers.set("Hello-There!", "General Kenobi"); + const client = new MeiliSearch({ + ...config, + apiKey: key, + requestInit: { headers }, + }); + + assert.isTrue(await client.isHealthy()); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + assert.strictEqual( + (requestInit!.headers! as Headers).get("Hello-There!"), + "General Kenobi", + ); }); - expect(client.httpRequest.headers["hello-there!"]).toBe("General Kenobi"); - const health = await client.isHealthy(); - expect(health).toBe(true); }); test(`${permission} key: No double slash when on host with domain and path and trailing slash`, async () => { @@ -187,7 +236,7 @@ describe.each([ test(`${permission} key: Empty string host should throw an error`, () => { expect(() => { new MeiliSearch({ host: "" }); - }).toThrow("The provided host is not valid."); + }).toThrow("The provided host is not valid"); }); }); @@ -203,13 +252,13 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( const client = new MeiliSearch({ ...config, apiKey: key, - requestConfig: { + requestInit: { headers: { "Hello-There!": "General Kenobi", }, }, }); - expect(client.config.requestConfig?.headers).toStrictEqual({ + expect(client.config.requestInit?.headers).toStrictEqual({ "Hello-There!": "General Kenobi", }); const health = await client.isHealthy(); @@ -256,45 +305,79 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( expect(documents.length).toBe(1); }); - test(`${permission} key: Create client with no custom client agents`, async () => { - const key = await getKey(permission); - const client = new MeiliSearch({ - ...config, - apiKey: key, - requestConfig: { - headers: {}, - }, + describe("Header tests", () => { + let fetchSpy: MockInstance; + + beforeAll(() => { + fetchSpy = vi.spyOn(globalThis, "fetch"); }); - expect(client.httpRequest.headers["X-Meilisearch-Client"]).toStrictEqual( - `Meilisearch JavaScript (v${PACKAGE_VERSION})`, - ); - }); + afterAll(() => fetchSpy.mockRestore()); - test(`${permission} key: Create client with empty custom client agents`, async () => { - const key = await getKey(permission); - const client = new MeiliSearch({ - ...config, - apiKey: key, - clientAgents: [], + test(`${permission} key: Create client with no custom client agents`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + requestInit: { + headers: {}, + }, + }); + + assert.isTrue(await client.isHealthy()); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + assert.strictEqual( + (requestInit!.headers! as Headers).get("X-Meilisearch-Client"), + `Meilisearch JavaScript (v${PACKAGE_VERSION})`, + ); }); - expect(client.httpRequest.headers["X-Meilisearch-Client"]).toStrictEqual( - `Meilisearch JavaScript (v${PACKAGE_VERSION})`, - ); - }); + test(`${permission} key: Create client with empty custom client agents`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + clientAgents: [], + }); - test(`${permission} key: Create client with custom client agents`, async () => { - const key = await getKey(permission); - const client = new MeiliSearch({ - ...config, - apiKey: key, - clientAgents: ["random plugin 1", "random plugin 2"], + assert.isTrue(await client.isHealthy()); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + assert.strictEqual( + (requestInit!.headers! as Headers).get("X-Meilisearch-Client"), + `Meilisearch JavaScript (v${PACKAGE_VERSION})`, + ); }); - expect(client.httpRequest.headers["X-Meilisearch-Client"]).toStrictEqual( - `random plugin 1 ; random plugin 2 ; Meilisearch JavaScript (v${PACKAGE_VERSION})`, - ); + test(`${permission} key: Create client with custom client agents`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + clientAgents: ["random plugin 1", "random plugin 2"], + }); + + assert.isTrue(await client.isHealthy()); + + assert.isDefined(fetchSpy.mock.lastCall); + const [, requestInit] = fetchSpy.mock.lastCall!; + + assert.isDefined(requestInit?.headers); + assert.instanceOf(requestInit!.headers, Headers); + assert.strictEqual( + (requestInit!.headers! as Headers).get("X-Meilisearch-Client"), + `random plugin 1 ; random plugin 2 ; Meilisearch JavaScript (v${PACKAGE_VERSION})`, + ); + }); }); describe("Test on indexes methods", () => { diff --git a/tests/dictionary.test.ts b/tests/dictionary.test.ts index b063d93af..f1c853e0b 100644 --- a/tests/dictionary.test.ts +++ b/tests/dictionary.test.ts @@ -28,7 +28,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( test(`${permission} key: Get default dictionary`, async () => { const client = await getClient(permission); - const response: string[] = await client.index(index.uid).getDictionary(); + const response = await client.index(index.uid).getDictionary(); expect(response).toEqual([]); }); diff --git a/tests/search.test.ts b/tests/search.test.ts index c51c84956..b1a13f967 100644 --- a/tests/search.test.ts +++ b/tests/search.test.ts @@ -5,6 +5,8 @@ import { beforeEach, afterAll, beforeAll, + assert, + vi, } from "vitest"; import { ErrorStatusCode, MatchingStrategies } from "../src/types.js"; import type { @@ -1435,10 +1437,61 @@ describe.each([ try { await client.health(); } catch (e: any) { - expect(e.cause.message).toEqual("Error: Request Timed Out"); + expect(e.cause.message).toEqual("request timed out after 1ms"); expect(e.name).toEqual("MeiliSearchRequestError"); } }); + + test(`${permission} key: search should be aborted on abort signal`, async () => { + const key = await getKey(permission); + const client = new MeiliSearch({ + ...config, + apiKey: key, + timeout: 1_000, + }); + const someErrorObj = {}; + + try { + const ac = new AbortController(); + ac.abort(someErrorObj); + + await client.multiSearch( + { queries: [{ indexUid: "doesn't matter" }] }, + { signal: ac.signal }, + ); + } catch (e: any) { + assert.strictEqual(e.cause, someErrorObj); + assert.strictEqual(e.name, "MeiliSearchRequestError"); + } + + // and now with a delayed abort, for this we have to stub fetch + vi.stubGlobal( + "fetch", + (_: unknown, requestInit?: RequestInit) => + new Promise((_, reject) => + requestInit?.signal?.addEventListener("abort", () => + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(requestInit.signal?.reason), + ), + ), + ); + + try { + const ac = new AbortController(); + + const promise = client.multiSearch( + { queries: [{ indexUid: "doesn't matter" }] }, + { signal: ac.signal }, + ); + setTimeout(() => ac.abort(someErrorObj), 1); + await promise; + } catch (e: any) { + assert.strictEqual(e.cause, someErrorObj); + assert.strictEqual(e.name, "MeiliSearchRequestError"); + } finally { + vi.unstubAllGlobals(); + } + }); }); describe.each([ diff --git a/tests/stop_words.test.ts b/tests/stop_words.test.ts index 47fbee68c..bbec0e38c 100644 --- a/tests/stop_words.test.ts +++ b/tests/stop_words.test.ts @@ -29,7 +29,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( test(`${permission} key: Get default stop words`, async () => { const client = await getClient(permission); - const response: string[] = await client.index(index.uid).getStopWords(); + const response = await client.index(index.uid).getStopWords(); expect(response).toEqual([]); }); diff --git a/tests/synonyms.test.ts b/tests/synonyms.test.ts index 6c7c516bb..d8ad653be 100644 --- a/tests/synonyms.test.ts +++ b/tests/synonyms.test.ts @@ -28,7 +28,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( test(`${permission} key: Get default synonyms`, async () => { const client = await getClient(permission); - const response: object = await client.index(index.uid).getSynonyms(); + const response = await client.index(index.uid).getSynonyms(); expect(response).toEqual({}); }); diff --git a/tests/task.test.ts b/tests/task.test.ts index aeecea5d2..84f3a20d8 100644 --- a/tests/task.test.ts +++ b/tests/task.test.ts @@ -616,10 +616,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])( test(`${permission} key: Try to cancel without filters and fail`, async () => { const client = await getClient(permission); - await expect( - // @ts-expect-error testing wrong argument type - client.cancelTasks(), - ).rejects.toHaveProperty( + await expect(client.cancelTasks()).rejects.toHaveProperty( "cause.code", ErrorStatusCode.MISSING_TASK_FILTERS, ); diff --git a/tests/unit.test.ts b/tests/unit.test.ts index aa570b191..0e0cc6f4e 100644 --- a/tests/unit.test.ts +++ b/tests/unit.test.ts @@ -1,27 +1,39 @@ -import { afterAll, expect, test } from "vitest"; +import { + afterAll, + assert, + beforeAll, + type MockInstance, + test, + vi, +} from "vitest"; import { clearAllIndexes, config, MeiliSearch, } from "./utils/meilisearch-test-utils.js"; -afterAll(() => { - return clearAllIndexes(config); +let fetchSpy: MockInstance; + +beforeAll(() => { + fetchSpy = vi.spyOn(globalThis, "fetch"); }); -test(`Client handles host URL with domain and path`, () => { - const customHost = `${config.host}/api/`; - const client = new MeiliSearch({ - host: customHost, - }); - expect(client.config.host).toBe(customHost); - expect(client.httpRequest.url.href).toBe(customHost); +afterAll(async () => { + fetchSpy.mockRestore(); + await clearAllIndexes(config); }); -test(`Client handles host URL with domain and path and no trailing slash`, () => { +test(`Client handles host URL with domain and path, and adds trailing slash`, async () => { const customHost = `${config.host}/api`; - const client = new MeiliSearch({ - host: customHost, - }); - expect(client.httpRequest.url.href).toBe(customHost + "/"); + const client = new MeiliSearch({ host: customHost }); + + assert.strictEqual(client.config.host, customHost); + + await client.isHealthy(); + + assert.isDefined(fetchSpy.mock.lastCall); + const [input] = fetchSpy.mock.lastCall!; + + assert.instanceOf(input, URL); + assert.strictEqual((input as URL).href, `${customHost}/health`); }); diff --git a/tests/utils/meilisearch-test-utils.ts b/tests/utils/meilisearch-test-utils.ts index 5dd9744b0..689ce1ea6 100644 --- a/tests/utils/meilisearch-test-utils.ts +++ b/tests/utils/meilisearch-test-utils.ts @@ -78,7 +78,7 @@ const clearAllIndexes = async (config: Config): Promise => { const client = new MeiliSearch(config); const { results } = await client.getRawIndexes(); - const indexes = results.map((elem) => elem.uid); + const indexes = results.map(({ uid }) => uid); const taskIds: number[] = []; for (const indexUid of indexes) { diff --git a/tsconfig.json b/tsconfig.json index dbcb92138..632e0b944 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "module": "node16", // Node.js 18 supports up to ES2022 according to https://www.npmjs.com/package/@tsconfig/node18 "target": "es2022", - "lib": ["ESNext", "dom"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "strict": true, "verbatimModuleSyntax": true }