diff --git a/package.json b/package.json index 465265ccc..09131b149 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "test": "vitest run --coverage", "types:watch": "nodemon --config nodemon.json", "types": "yarn tsc", - "test:env:browser": "yarn build && yarn --cwd tests/env/express && yarn --cwd tests/env/express test", + "test:env:browser": "yarn build && node scripts/copy-umd-file.js --to ./tests/env/express/public && yarn --cwd tests/env/express && yarn --cwd tests/env/express test", "test:watch": "vitest watch", "test:coverage": "yarn test", "test:ci": "yarn test", diff --git a/scripts/copy-umd-file.js b/scripts/copy-umd-file.js new file mode 100644 index 000000000..7572c70c2 --- /dev/null +++ b/scripts/copy-umd-file.js @@ -0,0 +1,16 @@ +const { parseArgs } = require("node:util"); +const { resolve, join, basename } = require("node:path"); +const { copyFileSync } = require("node:fs"); +const pkg = require("../package.json"); + +const { + values: { to }, +} = parseArgs({ options: { to: { type: "string" } } }); + +if (to === undefined) { + throw new Error("required argument `to` missing"); +} + +const umdAbsolutePath = resolve(__dirname, join("..", pkg.jsdelivr)); + +copyFileSync(umdAbsolutePath, join(to, basename(pkg.jsdelivr))); diff --git a/src/clients/client.ts b/src/clients/client.ts index 0cef642a9..053bfd941 100644 --- a/src/clients/client.ts +++ b/src/clients/client.ts @@ -18,25 +18,21 @@ import { ErrorStatusCode, TokenSearchRules, TokenOptions, - TasksQuery, - WaitOptions, KeyUpdate, IndexesQuery, IndexesResults, KeysQuery, KeysResults, - TasksResults, EnqueuedTaskObject, SwapIndexesParams, - CancelTasksQuery, - DeleteTasksQuery, MultiSearchParams, MultiSearchResponse, SearchResponse, FederatedMultiSearchParams, + ExtraRequestInit, } from "../types"; import { HttpRequests } from "../http-requests"; -import { TaskClient, Task } from "../task"; +import { TaskClient } from "../task"; import { EnqueuedTask } from "../enqueued-task"; class Client { @@ -98,7 +94,7 @@ class Client { * @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( @@ -114,13 +110,12 @@ class Client { * @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({ + relativeURL: "indexes", + params: parameters, + })) as IndexesResults; } /** @@ -132,7 +127,7 @@ class Client { */ async createIndex( uid: string, - options: IndexOptions = {}, + options?: IndexOptions, ): Promise { return await Index.create(uid, options, this.config); } @@ -146,7 +141,7 @@ class Client { */ async updateIndex( uid: string, - options: IndexOptions = {}, + options?: IndexOptions, ): Promise { return await new Index(this.config, uid).update(options); } @@ -188,7 +183,12 @@ class Client { */ async swapIndexes(params: SwapIndexesParams): Promise { const url = "/swap-indexes"; - return await this.httpRequest.post(url, params); + const taks = (await this.httpRequest.post({ + relativeURL: url, + body: params, + })) as EnqueuedTaskObject; + + return new EnqueuedTask(taks); } /// @@ -218,19 +218,21 @@ class Client { */ multiSearch = Record>( queries: MultiSearchParams, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise>; multiSearch = Record>( queries: FederatedMultiSearchParams, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise>; async multiSearch = Record>( queries: MultiSearchParams | FederatedMultiSearchParams, - config?: Partial, + extraRequestInit?: ExtraRequestInit, ): Promise | SearchResponse> { - const url = `multi-search`; - - return await this.httpRequest.post(url, queries, undefined, config); + return (await this.httpRequest.post({ + relativeURL: "multi-search", + body: queries, + extraRequestInit, + })) as MultiSearchResponse | SearchResponse; } /// @@ -243,8 +245,10 @@ class Client { * @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); } /** @@ -253,8 +257,10 @@ class Client { * @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); } /** @@ -265,13 +271,9 @@ class Client { * @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); } /** @@ -282,13 +284,9 @@ class Client { * @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); } /** @@ -297,8 +295,10 @@ class Client { * @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); } /** @@ -307,8 +307,10 @@ class Client { * @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); } /// @@ -321,9 +323,11 @@ class Client { * @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({ + relativeURL: "keys", + params: parameters, + })) as KeysResults; keys.results = keys.results.map((key) => ({ ...key, @@ -341,8 +345,9 @@ class Client { * @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({ + relativeURL: `keys/${keyOrUid}`, + })) as Key; } /** @@ -352,8 +357,10 @@ class Client { * @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({ + relativeURL: "keys", + body: options, + })) as Key; } /** @@ -364,8 +371,10 @@ class Client { * @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({ + relativeURL: `keys/${keyOrUid}`, + body: options, + })) as Key; } /** @@ -375,8 +384,7 @@ class Client { * @returns */ async deleteKey(keyOrUid: string): Promise { - const url = `keys/${keyOrUid}`; - return await this.httpRequest.delete(url); + await this.httpRequest.delete({ relativeURL: `keys/${keyOrUid}` }); } /// @@ -389,8 +397,7 @@ class Client { * @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({ relativeURL: "health" })) as Health; } /** @@ -400,9 +407,8 @@ class Client { */ 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; } @@ -418,8 +424,7 @@ class Client { * @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({ relativeURL: "stats" })) as Stats; } /// @@ -432,8 +437,7 @@ class Client { * @returns Promise returning object with version details */ async getVersion(): Promise { - const url = `version`; - return await this.httpRequest.get(url); + return (await this.httpRequest.get({ relativeURL: "version" })) as Version; } /// @@ -446,10 +450,10 @@ class Client { * @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({ + relativeURL: "dumps", + })) as EnqueuedTaskObject; + return new EnqueuedTask(task); } @@ -463,10 +467,9 @@ class Client { * @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({ + relativeURL: "snapshots", + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } diff --git a/src/http-requests.ts b/src/http-requests.ts index 971c2df11..fcb94c639 100644 --- a/src/http-requests.ts +++ b/src/http-requests.ts @@ -1,4 +1,10 @@ -import { Config, EnqueuedTaskObject } from "./types"; +import type { + Config, + HttpRequestsRequestInit, + MethodOptions, + RequestOptions, + URLSearchParamsRecord, +} from "./types"; import { PACKAGE_VERSION } from "./package-version"; import { @@ -7,156 +13,243 @@ import { MeiliSearchRequestError, } from "./errors"; -import { addTrailingSlash, addProtocolIfNotPresent } from "./utils"; - -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"; + +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 { +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("Content-Type", "application/json"); } // Creates the custom user agent with information on the package used. if (config.clientAgents && Array.isArray(config.clientAgents)) { const clients = config.clientAgents.concat(packageAgent); - headers[agentHeader] = clients.join(" ; "); + headers.set(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`, ); } 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 +// https://github.com/nodejs/node/issues/49557 +const TIMEOUT_OBJECT = {}; + +// Attach a timeout signal to `requestInit`, +// while preserving original signal functionality +// NOTE: This could be a short few straight forward lines using the following: +// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static +// https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static +// But these aren't yet widely supported enough perhaps, nor polyfillable +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); + }; +} + +export class HttpRequests { + #url: URL; + #requestInit: HttpRequestsRequestInit; + #requestFn: typeof fetch; + #isCustomRequestFnProvided: boolean; + #requestTimeout?: number; 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.#requestFn = + config.httpClient ?? + // in browsers `fetch` can only be called with a `this` pointing to `window` + fetch.bind(typeof window !== "undefined" ? window : globalThis); + this.#isCustomRequestFnProvided = config.httpClient !== undefined; + this.#requestTimeout = config.timeout; } - async request({ + // combine headers, with the following priority: + // 1. `headers` - primary headers provided by functions in Index and Client + // 2. `this.#requestInit.headers` - main headers of this class + // 3. `extraHeaders` - extra headers provided in search functions by users + #getHeaders( + headers?: HeadersInit, + extraHeaders?: HeadersInit, + ): { finalHeaders: Headers; isCustomContentTypeProvided: boolean } { + let isCustomContentTypeProvided: boolean; + + if (headers !== undefined || extraHeaders !== undefined) { + headers = new Headers(headers); + isCustomContentTypeProvided = headers.has("Content-Type"); + + for (const [key, val] of this.#requestInit.headers.entries()) { + if (!headers.has(key)) { + headers.set(key, val); + } + } + + if (extraHeaders !== undefined) { + for (const [key, val] of new Headers(extraHeaders).entries()) { + if (!headers.has(key)) { + headers.set(key, val); + } + } + } + } else { + isCustomContentTypeProvided = false; + } + + const finalHeaders = headers ?? this.#requestInit.headers; + + return { finalHeaders, isCustomContentTypeProvided }; + } + + async #request({ + relativeURL, method, - url, params, + headers, 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, + }: RequestOptions): Promise { + const url = new URL(relativeURL, 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 headers = { ...this.headers, ...config.headers }; - const responsePromise = this.fetchWithTimeout( - constructURL.toString(), - { - ...config, - ...this.requestConfig, - method, - body, - headers, - }, - this.requestTimeout, + const { finalHeaders, isCustomContentTypeProvided } = this.#getHeaders( + headers, + extraRequestInit?.headers, ); - const response = await responsePromise.catch((error: unknown) => { - throw new MeiliSearchRequestError(constructURL.toString(), error); - }); + const requestInit: RequestInit = { + method, + body: + // in case a custom content-type is provided do not stringify body + typeof body !== "string" || !isCustomContentTypeProvided + ? // this will throw an error for any value that is not serializable + JSON.stringify(body) + : body, + ...extraRequestInit, + ...this.#requestInit, + headers: finalHeaders, + }; + + const startTimeout = + this.#requestTimeout !== undefined + ? getTimeoutFn(requestInit, this.#requestTimeout) + : null; + + const responsePromise = this.#requestFn(url, requestInit); + const stopTimeout = startTimeout?.(); + + const response = await 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?.()); // 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) { + if (this.#isCustomRequestFnProvided) { return response; } @@ -171,149 +264,23 @@ class HttpRequests { 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); - }); - }); - } - - 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, - }); + get(options: MethodOptions) { + 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, - }); + post(options: MethodOptions) { + 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, - }); + put(options: MethodOptions) { + 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, - }); + patch(options: MethodOptions) { + 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, - }); + delete(options: MethodOptions) { + return this.#request({ ...options, method: "DELETE" }); } } - -export { HttpRequests, toQueryParams }; diff --git a/src/indexes.ts b/src/indexes.ts index f965706b9..cf0941ced 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -56,8 +56,9 @@ import { SearchSimilarDocumentsParams, LocalizedAttributes, UpdateDocumentsByFunctionOptions, + EnqueuedTaskObject, + ExtraRequestInit, } from "./types"; -import { removeUndefinedFromObject } from "./utils"; import { HttpRequests } from "./http-requests"; import { Task, TaskClient } from "./task"; import { EnqueuedTask } from "./enqueued-task"; @@ -100,16 +101,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({ + relativeURL: `indexes/${this.uid}/search`, + body: { q: query, ...options }, + extraRequestInit, + })) as SearchResponse; } /** @@ -126,10 +124,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)) @@ -152,11 +149,11 @@ class Index = Record> { attributesToSearchOn: options?.attributesToSearchOn?.join(","), }; - return await this.httpRequest.get>( - url, - removeUndefinedFromObject(getParams), - config, - ); + return (await this.httpRequest.get({ + relativeURL: `indexes/${this.uid}/search`, + params: getParams, + extraRequestInit, + })) as SearchResponse; } /** @@ -168,16 +165,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({ + relativeURL: `indexes/${this.uid}/facet-search`, + body: params, + extraRequestInit, + })) as SearchForFacetValuesResponse; } /** @@ -190,13 +184,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({ + relativeURL: `indexes/${this.uid}/similar`, + body: params, + })) as SearchResponse; } /// @@ -209,8 +200,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({ + relativeURL: `indexes/${this.uid}`, + })) as IndexObject; this.primaryKey = res.primaryKey; this.updatedAt = new Date(res.updatedAt); this.createdAt = new Date(res.createdAt); @@ -250,9 +242,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({ + relativeURL: "indexes", + body: { ...options, uid }, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -263,13 +257,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); + async update(data?: IndexOptions): Promise { + const task = (await this.httpRequest.patch({ + relativeURL: `indexes/${this.uid}`, + body: data, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /** @@ -278,8 +272,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({ + relativeURL: `indexes/${this.uid}`, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -294,7 +289,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] }); } @@ -352,8 +347,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({ + relativeURL: `indexes/${this.uid}/stats`, + })) as IndexStats; } /// @@ -363,24 +359,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({ + relativeURL: `${relativeBaseURL}/fetch`, + body: params, + })) as ResourceResults; } catch (e) { if (e instanceof MeiliSearchRequestError) { e.message = versionErrorHintMessage(e.message, "getDocuments"); @@ -390,19 +384,12 @@ 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({ + relativeURL: relativeBaseURL, + params, + })) as ResourceResults; } } @@ -417,8 +404,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(","); @@ -426,13 +411,10 @@ class Index = Record> { return undefined; })(); - return await this.httpRequest.get( - url, - removeUndefinedFromObject({ - ...parameters, - fields, - }), - ); + return (await this.httpRequest.get({ + relativeURL: `indexes/${this.uid}/documents/${documentId}`, + params: { ...parameters, fields }, + })) as D; } /** @@ -446,8 +428,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({ + relativeURL: `indexes/${this.uid}/documents`, + params: options, + body: documents, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -467,13 +452,12 @@ 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({ + relativeURL: `indexes/${this.uid}/documents`, + body: documents, + params: queryParams, + headers: { "Content-Type": contentType }, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -511,8 +495,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({ + relativeURL: `indexes/${this.uid}/documents`, + params: options, + body: documents, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -554,13 +541,12 @@ 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({ + relativeURL: `indexes/${this.uid}/documents`, + body: documents, + params: queryParams, + headers: { "Content-Type": contentType }, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -572,12 +558,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/documents/${documentId}`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /** @@ -600,10 +585,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({ + relativeURL: `indexes/${this.uid}/${endpoint}`, + body: params, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } catch (e) { @@ -623,12 +610,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/documents`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /** @@ -646,8 +632,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({ + relativeURL: `indexes/${this.uid}/documents/edit`, + body: options, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -662,8 +650,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({ + relativeURL: `indexes/${this.uid}/settings`, + })) as Settings; } /** @@ -673,12 +662,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({ + relativeURL: `indexes/${this.uid}/settings`, + body: settings, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /** @@ -687,12 +676,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({ + relativeURL: `indexes/${this.uid}/settings`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -705,8 +693,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({ + relativeURL: `indexes/${this.uid}/settings/pagination`, + })) as PaginationSettings; } /** @@ -718,8 +707,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({ + relativeURL: `indexes/${this.uid}/settings/pagination`, + body: pagination, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -730,8 +721,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({ + relativeURL: `indexes/${this.uid}/settings/pagination`, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -743,11 +735,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({ + relativeURL: `indexes/${this.uid}/settings/synonyms`, + })) as Record; } /** @@ -757,8 +750,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({ + relativeURL: `indexes/${this.uid}/settings/synonyms`, + body: synonyms, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -769,12 +764,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/settings/synonyms`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /// @@ -787,8 +781,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({ + relativeURL: `indexes/${this.uid}/settings/stop-words`, + })) as string[]; } /** @@ -798,8 +793,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({ + relativeURL: `indexes/${this.uid}/settings/stop-words`, + body: stopWords, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -810,12 +807,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({ + relativeURL: `indexes/${this.uid}/settings/stop-words`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -828,8 +824,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({ + relativeURL: `indexes/${this.uid}/settings/ranking-rules`, + })) as string[]; } /** @@ -840,8 +837,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({ + relativeURL: `indexes/${this.uid}/settings/ranking-rules`, + body: rankingRules, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -852,12 +851,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({ + relativeURL: `indexes/${this.uid}/settings/ranking-rules`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -869,9 +867,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({ + relativeURL: `indexes/${this.uid}/settings/distinct-attribute`, + })) as DistinctAttribute; } /** @@ -883,8 +882,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({ + relativeURL: `indexes/${this.uid}/settings/distinct-attribute`, + body: distinctAttribute, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -895,12 +896,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({ + relativeURL: `indexes/${this.uid}/settings/distinct-attribute`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -913,8 +913,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({ + relativeURL: `indexes/${this.uid}/settings/filterable-attributes`, + })) as string[]; } /** @@ -927,8 +928,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({ + relativeURL: `indexes/${this.uid}/settings/filterable-attributes`, + body: filterableAttributes, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -939,12 +942,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({ + relativeURL: `indexes/${this.uid}/settings/filterable-attributes`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -957,8 +959,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({ + relativeURL: `indexes/${this.uid}/settings/sortable-attributes`, + })) as string[]; } /** @@ -971,8 +974,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({ + relativeURL: `indexes/${this.uid}/settings/sortable-attributes`, + body: sortableAttributes, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -983,12 +988,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/settings/sortable-attributes`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /// @@ -1001,8 +1005,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({ + relativeURL: `indexes/${this.uid}/settings/searchable-attributes`, + })) as string[]; } /** @@ -1015,8 +1020,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({ + relativeURL: `indexes/${this.uid}/settings/searchable-attributes`, + body: searchableAttributes, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1027,12 +1034,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/settings/searchable-attributes`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /// @@ -1045,8 +1051,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({ + relativeURL: `indexes/${this.uid}/settings/displayed-attributes`, + })) as string[]; } /** @@ -1059,8 +1066,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({ + relativeURL: `indexes/${this.uid}/settings/displayed-attributes`, + body: displayedAttributes, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1071,12 +1080,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/settings/displayed-attributes`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /// @@ -1089,8 +1097,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({ + relativeURL: `indexes/${this.uid}/settings/typo-tolerance`, + })) as TypoTolerance; } /** @@ -1103,12 +1112,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({ + relativeURL: `indexes/${this.uid}/settings/typo-tolerance`, + body: typoTolerance, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /** @@ -1117,12 +1126,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({ + relativeURL: `indexes/${this.uid}/settings/typo-tolerance`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -1135,8 +1143,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({ + relativeURL: `indexes/${this.uid}/settings/faceting`, + })) as Faceting; } /** @@ -1146,8 +1155,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({ + relativeURL: `indexes/${this.uid}/settings/faceting`, + body: faceting, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1158,8 +1169,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({ + relativeURL: `indexes/${this.uid}/settings/faceting`, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1174,8 +1186,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({ + relativeURL: `indexes/${this.uid}/settings/separator-tokens`, + })) as string[]; } /** @@ -1187,8 +1200,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({ + relativeURL: `indexes/${this.uid}/settings/separator-tokens`, + body: separatorTokens, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1199,12 +1214,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({ + relativeURL: `indexes/${this.uid}/settings/separator-tokens`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -1217,8 +1231,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({ + relativeURL: `indexes/${this.uid}/settings/non-separator-tokens`, + })) as string[]; } /** @@ -1230,8 +1245,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({ + relativeURL: `indexes/${this.uid}/settings/non-separator-tokens`, + body: nonSeparatorTokens, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1242,12 +1259,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({ + relativeURL: `indexes/${this.uid}/settings/non-separator-tokens`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -1260,8 +1276,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({ + relativeURL: `indexes/${this.uid}/settings/dictionary`, + })) as string[]; } /** @@ -1271,8 +1288,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({ + relativeURL: `indexes/${this.uid}/settings/dictionary`, + body: dictionary, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1283,12 +1302,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); + const task = (await this.httpRequest.delete({ + relativeURL: `indexes/${this.uid}/settings/dictionary`, + })) as EnqueuedTaskObject; - task.enqueuedAt = new Date(task.enqueuedAt); - - return task; + return new EnqueuedTask(task); } /// @@ -1301,8 +1319,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({ + relativeURL: `indexes/${this.uid}/settings/proximity-precision`, + })) as ProximityPrecision; } /** @@ -1315,8 +1334,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({ + relativeURL: `indexes/${this.uid}/settings/proximity-precision`, + body: proximityPrecision, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1327,12 +1348,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({ + relativeURL: `indexes/${this.uid}/settings/proximity-precision`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -1345,8 +1365,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({ + relativeURL: `indexes/${this.uid}/settings/embedders`, + })) as Embedders; } /** @@ -1356,8 +1377,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({ + relativeURL: `indexes/${this.uid}/settings/embedders`, + body: embedders, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1368,12 +1391,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({ + relativeURL: `indexes/${this.uid}/settings/embedders`, + })) as EnqueuedTaskObject; - return task; + return new EnqueuedTask(task); } /// @@ -1386,8 +1408,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({ + relativeURL: `indexes/${this.uid}/settings/search-cutoff-ms`, + })) as SearchCutoffMs; } /** @@ -1399,8 +1422,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({ + relativeURL: `indexes/${this.uid}/settings/search-cutoff-ms`, + body: searchCutoffMs, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1411,8 +1436,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({ + relativeURL: `indexes/${this.uid}/settings/search-cutoff-ms`, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1427,8 +1453,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({ + relativeURL: `indexes/${this.uid}/settings/localized-attributes`, + })) as LocalizedAttributes; } /** @@ -1440,8 +1467,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({ + relativeURL: `indexes/${this.uid}/settings/localized-attributes`, + body: localizedAttributes, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -1452,8 +1481,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({ + relativeURL: `indexes/${this.uid}/settings/localized-attributes`, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } diff --git a/src/task.ts b/src/task.ts index de3388add..c1804bf37 100644 --- a/src/task.ts +++ b/src/task.ts @@ -9,8 +9,9 @@ import { CancelTasksQuery, TasksResultsObject, DeleteTasksQuery, + EnqueuedTaskObject, } from "./types"; -import { HttpRequests, toQueryParams } from "./http-requests"; +import { HttpRequests } from "./http-requests"; import { sleep } from "./utils"; import { EnqueuedTask } from "./enqueued-task"; @@ -57,24 +58,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({ + relativeURL: `tasks/${uid}`, + })) as TaskObject; 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({ + relativeURL: "tasks", + params, + })) as TasksResultsObject; return { ...tasks, @@ -137,17 +137,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({ + relativeURL: "tasks/cancel", + params, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } @@ -155,17 +152,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({ + relativeURL: "tasks", + params, + })) as EnqueuedTaskObject; return new EnqueuedTask(task); } } diff --git a/src/types/types.ts b/src/types/types.ts index b6feccdd6..578878e9d 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -6,15 +6,45 @@ import { Task } from "../task"; +export type URLSearchParamsRecord = Record< + string, + | string + | string[] + | Array + | number + | number[] + | boolean + | Date + | null + | undefined +>; + +export type ExtraRequestInit = Omit; +export type BaseRequestInit = Omit; +export type HttpRequestsRequestInit = Omit & { + headers: Headers; +}; + export type Config = { host: string; apiKey?: string; clientAgents?: string[]; - requestConfig?: Partial>; - httpClient?: (input: string, init?: RequestInit) => Promise; + requestInit?: BaseRequestInit; + httpClient?: typeof fetch; timeout?: number; }; +export type RequestOptions = { + relativeURL: string; + method?: string; + params?: URLSearchParamsRecord; + headers?: HeadersInit; + body?: unknown; + extraRequestInit?: ExtraRequestInit; +}; + +export type MethodOptions = Omit; + /// /// Resources /// @@ -359,9 +389,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; @@ -524,9 +552,9 @@ export type TasksQuery = { from?: number; }; -export type CancelTasksQuery = Omit & {}; +export type CancelTasksQuery = Omit; -export type DeleteTasksQuery = Omit & {}; +export type DeleteTasksQuery = Omit; export type EnqueuedTaskObject = { taskUid: number; diff --git a/src/utils.ts b/src/utils.ts index b5de6d680..7e8623faa 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)); } @@ -34,10 +22,4 @@ function validateUuid4(uuid: string): boolean { return regexExp.test(uuid); } -export { - sleep, - removeUndefinedFromObject, - addProtocolIfNotPresent, - addTrailingSlash, - validateUuid4, -}; +export { sleep, addProtocolIfNotPresent, addTrailingSlash, validateUuid4 }; diff --git a/tests/client.test.ts b/tests/client.test.ts index 09693d124..b0e4380a8 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 { ErrorStatusCode, Health, Version, Stats, TaskTypes } from "../src"; import { PACKAGE_VERSION } from "../src/package-version"; import { @@ -50,50 +60,85 @@ 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", + }, }, - }, + }); + + 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 (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 () => { @@ -186,7 +231,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"); }); }); @@ -202,13 +247,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(); @@ -255,45 +300,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 1215eedba..9abd467e8 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/env/express/.gitignore b/tests/env/express/.gitignore index 3c3629e64..e6252fa2d 100644 --- a/tests/env/express/.gitignore +++ b/tests/env/express/.gitignore @@ -1 +1,2 @@ node_modules +public/meilisearch.umd.js diff --git a/tests/search.test.ts b/tests/search.test.ts index 7bbc7c573..4e18d58c1 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"; import { EnqueuedTask } from "../src/enqueued-task"; @@ -1269,10 +1271,66 @@ 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 already 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"); + } + + vi.stubGlobal("fetch", (_, requestInit?: RequestInit) => { + return new Promise((_, reject) => { + setInterval(() => { + if (requestInit?.signal?.aborted) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(requestInit.signal.reason); + } + }, 5); + }); + }); + + const clientWithStubbedFetch = new MeiliSearch({ + ...config, + apiKey: key, + timeout: 1_000, + }); + + try { + const ac = new AbortController(); + + const promise = clientWithStubbedFetch.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 62d53ade5..0595c0bc9 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 09fe4e70e..ada7b7c83 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 18968f5e4..ededbd452 100644 --- a/tests/task.test.ts +++ b/tests/task.test.ts @@ -589,10 +589,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 3134974c5..f014cdf6d 100644 --- a/tests/unit.test.ts +++ b/tests/unit.test.ts @@ -1,27 +1,32 @@ -import { afterAll, expect, test } from "vitest"; +import { afterAll, assert, beforeAll, MockInstance, test, vi } from "vitest"; import { clearAllIndexes, config, MeiliSearch, } from "./utils/meilisearch-test-utils"; -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 e2a9cb834..bfbab4258 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 368aaf464..0152bf4ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ // This matters for the generated CJS and ESM file, UMD file is down-leveled further to support IE11 // (only the syntax, URL, URLSearchParams, fetch is not IE11 compatible) with the help of Babel. "target": "es2022", - "lib": ["ESNext", "dom"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], "strict": true, "noImplicitReturns": true },