Skip to content

Commit 14bbed2

Browse files
committed
feat: added support for custom request config
feat: added support for custom http client
1 parent c11cc13 commit 14bbed2

File tree

5 files changed

+159
-18
lines changed

5 files changed

+159
-18
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,46 @@ index
327327
controller.abort()
328328
```
329329

330+
331+
### Using Meilisearch behind a proxy <!-- omit in toc -->
332+
333+
#### Custom request config <!-- omit in toc -->
334+
335+
You can provide a custom request configuration. for example, with custom headers.
336+
337+
```ts
338+
const client: MeiliSearch = new MeiliSearch({
339+
host: 'http://localhost:3000/api/meilisearch/proxy',
340+
requestConfig: {
341+
headers: {
342+
Authorization: AUTH_TOKEN
343+
},
344+
// OR
345+
credentials: 'include'
346+
}
347+
})
348+
```
349+
350+
#### Custom http client <!-- omit in toc -->
351+
352+
You can use your own HTTP client, for example, with [`axios`](https://github.com/axios/axios).
353+
354+
```ts
355+
const client: MeiliSearch = new MeiliSearch({
356+
host: 'http://localhost:3000/api/meilisearch/proxy',
357+
httpClient: async (url, opts) => {
358+
const response = await $axios.request({
359+
url,
360+
data: opts?.body,
361+
headers: opts?.headers,
362+
method: (opts?.method?.toLocaleUpperCase() as Method) ?? 'GET'
363+
})
364+
365+
return response.data
366+
}
367+
})
368+
```
369+
330370
## 🤖 Compatibility with Meilisearch
331371

332372
This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.

src/clients/browser-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Client } from './client'
21
import { Config } from '../types'
2+
import { Client } from './client'
33

44
class MeiliSearch extends Client {
55
constructor(config: Config) {

src/http-requests.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,34 @@ function constructHostURL(host: string): string {
4040
}
4141
}
4242

43+
function cloneAndParseHeaders(headers: HeadersInit): Record<string, string> {
44+
if (Array.isArray(headers)) {
45+
return headers.reduce((acc, headerPair) => {
46+
acc[headerPair[0]] = headerPair[1]
47+
return acc
48+
}, {} as Record<string, string>)
49+
} else if ('has' in headers) {
50+
const clonedHeaders: Record<string, string> = {}
51+
;(headers as Headers).forEach((value, key) => (clonedHeaders[key] = value))
52+
return clonedHeaders
53+
} else {
54+
return Object.assign({}, headers)
55+
}
56+
}
57+
4358
function createHeaders(config: Config): Record<string, any> {
4459
const agentHeader = 'X-Meilisearch-Client'
4560
const packageAgent = `Meilisearch JavaScript (v${PACKAGE_VERSION})`
4661
const contentType = 'Content-Type'
47-
config.headers = config.headers || {}
48-
49-
const headers: Record<string, any> = Object.assign({}, config.headers) // Create a hard copy and not a reference to config.headers
62+
const authorization = 'Authorization'
63+
const headers = cloneAndParseHeaders(config.requestConfig?.headers ?? {})
5064

51-
if (config.apiKey) {
52-
headers['Authorization'] = `Bearer ${config.apiKey}`
65+
// do not override if user provided the header
66+
if (config.apiKey && !headers[authorization]) {
67+
headers[authorization] = `Bearer ${config.apiKey}`
5368
}
5469

55-
if (!config.headers[contentType]) {
70+
if (!headers[contentType]) {
5671
headers['Content-Type'] = 'application/json'
5772
}
5873

@@ -76,9 +91,13 @@ function createHeaders(config: Config): Record<string, any> {
7691
class HttpRequests {
7792
headers: Record<string, any>
7893
url: URL
94+
requestConfig?: Config['requestConfig']
95+
httpClient?: Required<Config>['httpClient']
7996

8097
constructor(config: Config) {
8198
this.headers = createHeaders(config)
99+
this.requestConfig = config.requestConfig
100+
this.httpClient = config.httpClient
82101

83102
try {
84103
const host = constructHostURL(config.host)
@@ -111,12 +130,23 @@ class HttpRequests {
111130
}
112131

113132
try {
114-
const response: any = await fetch(constructURL.toString(), {
133+
const fetchFn = this.httpClient ? this.httpClient : fetch
134+
const result = fetchFn(constructURL.toString(), {
115135
...config,
136+
...this.requestConfig,
116137
method,
117138
body: JSON.stringify(body),
118139
headers: this.headers,
119-
}).then((res) => httpResponseErrorHandler(res))
140+
})
141+
142+
// When using a custom HTTP client, the response is returned to allow the user to parse/handle it as they see fit
143+
if (this.httpClient) {
144+
return await result
145+
}
146+
147+
const response = await result.then((res: any) =>
148+
httpResponseErrorHandler(res)
149+
)
120150
const parsedBody = await response.json().catch(() => undefined)
121151

122152
return parsedBody

src/types/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export type Config = {
1010
host: string
1111
apiKey?: string
1212
clientAgents?: string[]
13-
headers?: Record<string, any>
13+
requestConfig?: Partial<Omit<RequestInit, 'body' | 'method'>>
14+
httpClient?: (input: string, init?: RequestInit) => Promise<any>
1415
}
1516

1617
///

tests/client.test.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,48 @@ describe.each([
4949
expect(health).toBe(true)
5050
})
5151

52-
test(`${permission} key: Create client with custom headers`, async () => {
52+
test(`${permission} key: Create client with custom headers (object)`, async () => {
5353
const key = await getKey(permission)
5454
const client = new MeiliSearch({
5555
...config,
5656
apiKey: key,
57-
headers: {
58-
Expect: '200-OK',
57+
requestConfig: {
58+
headers: {
59+
Expect: '200-OK',
60+
},
61+
},
62+
})
63+
expect(client.httpRequest.headers['Expect']).toBe('200-OK')
64+
const health = await client.isHealthy()
65+
expect(health).toBe(true)
66+
})
67+
68+
test(`${permission} key: Create client with custom headers (array)`, async () => {
69+
const key = await getKey(permission)
70+
const client = new MeiliSearch({
71+
...config,
72+
apiKey: key,
73+
requestConfig: {
74+
headers: [['Expect', '200-OK']],
5975
},
6076
})
61-
expect(client.config.headers).toStrictEqual({ Expect: '200-OK' })
77+
expect(client.httpRequest.headers['Expect']).toBe('200-OK')
78+
const health = await client.isHealthy()
79+
expect(health).toBe(true)
80+
})
81+
82+
test(`${permission} key: Create client with custom headers (Headers)`, async () => {
83+
const key = await getKey(permission)
84+
const headers = new Headers()
85+
headers.append('Expect', '200-OK')
86+
const client = new MeiliSearch({
87+
...config,
88+
apiKey: key,
89+
requestConfig: {
90+
headers,
91+
},
92+
})
93+
expect(client.httpRequest.headers.expect).toBe('200-OK')
6294
const health = await client.isHealthy()
6395
expect(health).toBe(true)
6496
})
@@ -169,11 +201,15 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])(
169201
const client = new MeiliSearch({
170202
...config,
171203
apiKey: key,
172-
headers: {
173-
Expect: '200-OK',
204+
requestConfig: {
205+
headers: {
206+
Expect: '200-OK',
207+
},
174208
},
175209
})
176-
expect(client.config.headers).toStrictEqual({ Expect: '200-OK' })
210+
expect(client.config.requestConfig?.headers).toStrictEqual({
211+
Expect: '200-OK',
212+
})
177213
const health = await client.isHealthy()
178214

179215
expect(health).toBe(true)
@@ -186,12 +222,46 @@ describe.each([{ permission: 'Master' }, { permission: 'Admin' }])(
186222
expect(results.length).toBe(1)
187223
})
188224

225+
test(`${permission} key: Create client with custom http client`, async () => {
226+
const key = await getKey(permission)
227+
const client = new MeiliSearch({
228+
...config,
229+
apiKey: key,
230+
async httpClient(url, init) {
231+
const result = await fetch(url, init)
232+
return result.json()
233+
},
234+
})
235+
const health = await client.isHealthy()
236+
237+
expect(health).toBe(true)
238+
239+
const task = await client.createIndex('test')
240+
await client.waitForTask(task.taskUid)
241+
242+
const { results } = await client.getIndexes()
243+
244+
expect(results.length).toBe(1)
245+
246+
const index = await client.getIndex('test')
247+
248+
const { taskUid } = await index.addDocuments([
249+
{ id: 1, title: 'index_2' },
250+
])
251+
await client.waitForTask(taskUid)
252+
253+
const { results: documents } = await index.getDocuments()
254+
expect(documents.length).toBe(1)
255+
})
256+
189257
test(`${permission} key: Create client with no custom client agents`, async () => {
190258
const key = await getKey(permission)
191259
const client = new MeiliSearch({
192260
...config,
193261
apiKey: key,
194-
headers: {},
262+
requestConfig: {
263+
headers: {},
264+
},
195265
})
196266

197267
expect(client.httpRequest.headers['X-Meilisearch-Client']).toStrictEqual(

0 commit comments

Comments
 (0)