Skip to content

Commit d8fd8d7

Browse files
authored
Feat: Make SearchParams and GetDocumentsParams generic (#451)
* fix: allow to type the search response when using index search query * fix: fallback on any type if no type provided * feat: update Index Class with generic passed to IndexInterface. * feat: make SearchParams generic - attributesToRetrieve, attributesToCrop, attributesToHighlight extends keyof T - support asterisk in attributesToCrop, attributesToHighlight * feat: make GetDocumentsParams generic. - attributesToRetrieve expects to be generic's properties * feat: add type-fest as DevDep * feat: GetDocumentsResponse adjusted by attributesToRetrieve * chore: uninstall not used type-fest * fix: revert added import section in readme using omit in toc * feat: SearchResponse adjusted by attributesToRetrieve * fix: use type inference in UTs
1 parent a9b0c24 commit d8fd8d7

File tree

4 files changed

+75
-45
lines changed

4 files changed

+75
-45
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ NB: you can also download MeiliSearch from **Homebrew** or **APT**.
6868
import MeiliSearch from 'meilisearch'
6969

7070
const client = new MeiliSearch({
71-
host: 'http://127.0.0.1:7700',
72-
apiKey: 'masterKey',
73-
})
71+
host: 'http://127.0.0.1:7700',
72+
apiKey: 'masterKey',
73+
})
7474
```
7575

7676
#### HTML Import <!-- omit in toc -->
@@ -91,12 +91,12 @@ const client = new MeiliSearch({
9191
#### Back-End CommonJs <!-- omit in toc -->
9292

9393
```javascript
94-
const MeiliSearch = require('meilisearch');
94+
const MeiliSearch = require('meilisearch')
9595

9696
const client = new MeiliSearch({
97-
host: 'http://127.0.0.1:7700',
98-
apiKey: 'masterKey',
99-
})
97+
host: 'http://127.0.0.1:7700',
98+
apiKey: 'masterKey',
99+
})
100100
```
101101

102102
## 🎬 Getting started

src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ class Index<T> extends MeiliAxiosWrapper implements Types.IndexInterface<T> {
7272
* @memberof Index
7373
* @method search
7474
*/
75-
async search(
75+
async search<P extends Types.SearchParams<T>>(
7676
query: string,
77-
options?: Types.SearchParams
78-
): Promise<Types.SearchResponse<T>> {
77+
options?: P
78+
): Promise<Types.SearchResponse<T, P>> {
7979
const url = `/indexes/${this.uid}/search`
8080

8181
const params: Types.SearchRequest = {
@@ -193,9 +193,9 @@ class Index<T> extends MeiliAxiosWrapper implements Types.IndexInterface<T> {
193193
* @memberof Index
194194
* @method getDocuments
195195
*/
196-
async getDocuments(
197-
options?: Types.GetDocumentsParams
198-
): Promise<Array<Types.Document<T>>> {
196+
async getDocuments<P extends Types.GetDocumentsParams<T>>(
197+
options?: P
198+
): Promise<Types.GetDocumentsResponse<T, P>> {
199199
const url = `/indexes/${this.uid}/documents`
200200
let attr
201201
if (options !== undefined && Array.isArray(options.attributesToRetrieve)) {

src/types.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,17 @@ export interface AddDocumentParams {
5353

5454
export type FacetFilter = (string | string[])[]
5555

56-
export interface SearchParams {
56+
export interface SearchParams<T> {
5757
offset?: number
5858
limit?: number
59-
attributesToRetrieve?: string[] | string
60-
attributesToCrop?: string[] | string
59+
attributesToRetrieve?: Extract<keyof T, string>[] | Extract<keyof T, string>
60+
attributesToCrop?:
61+
| (Extract<keyof T, string> | '*')[]
62+
| (Extract<keyof T, string> | '*')
6163
cropLength?: number
62-
attributesToHighlight?: string[] | string
64+
attributesToHighlight?:
65+
| (Extract<keyof T, string> | '*')[]
66+
| (Extract<keyof T, string> | '*')
6367
filters?: string
6468
facetFilters?: string | FacetFilter | FacetFilter[]
6569
facetsDistribution?: string[]
@@ -82,8 +86,16 @@ export interface SearchRequest {
8286

8387
export type Hit<T> = T & { _formatted?: T }
8488

85-
export interface SearchResponse<T = any> {
86-
hits: Array<Hit<T>>
89+
export interface SearchResponse<T, P extends SearchParams<T>> {
90+
hits: P['attributesToRetrieve'] extends keyof T
91+
? Array<
92+
Hit<
93+
Pick<T, Exclude<keyof T, Exclude<keyof T, P['attributesToRetrieve']>>>
94+
>
95+
>
96+
: P['attributesToRetrieve'] extends Array<infer K>
97+
? Array<Hit<Pick<T, Exclude<keyof T, Exclude<keyof T, K>>>>>
98+
: Array<Hit<T>>
8799
offset: number
88100
limit: number
89101
processingTimeMs: number
@@ -99,12 +111,25 @@ export interface FieldFrequency {
99111
/*
100112
** Documents
101113
*/
102-
export interface GetDocumentsParams {
114+
export interface GetDocumentsParams<T> {
103115
offset?: number
104116
limit?: number
105-
attributesToRetrieve?: string[]
117+
attributesToRetrieve?: Extract<keyof T, string>[] | Extract<keyof T, string>
106118
}
107119

120+
export type GetDocumentsResponse<
121+
T,
122+
P extends GetDocumentsParams<T>
123+
> = P['attributesToRetrieve'] extends keyof T
124+
? Array<
125+
Document<
126+
Pick<T, Exclude<keyof T, Exclude<keyof T, P['attributesToRetrieve']>>>
127+
>
128+
>
129+
: P['attributesToRetrieve'] extends Array<infer K>
130+
? Array<Document<Pick<T, Exclude<keyof T, Exclude<keyof T, K>>>>>
131+
: Array<Document<T>>
132+
108133
export type DocumentLike = { [Key in string]?: DocumentField }
109134
export interface DocumentArray extends Array<DocumentField> {}
110135
export type DocumentField =
@@ -116,9 +141,9 @@ export type DocumentField =
116141
| DocumentArray
117142

118143
export type Document<T> = DocumentLike &
119-
{
120-
[key in keyof T]: T[key]
121-
}
144+
{
145+
[key in keyof T]: T[key]
146+
}
122147

123148
/*
124149
** Settings
@@ -258,12 +283,17 @@ export interface IndexInterface<T = any> extends MeiliAxiosWrapperInterface {
258283
uid: string
259284
getUpdateStatus: (updateId: number) => Promise<Update>
260285
getAllUpdateStatus: () => Promise<Update[]>
261-
search: (query: string, options?: SearchParams) => Promise<SearchResponse<T>>
286+
search: <P extends SearchParams<T>>(
287+
query: string,
288+
options?: P
289+
) => Promise<SearchResponse<T, P>>
262290
show: () => Promise<IndexResponse>
263291
updateIndex: (indexData: IndexOptions) => Promise<IndexResponse>
264292
deleteIndex: () => Promise<string>
265293
getStats: () => Promise<IndexStats>
266-
getDocuments: (options?: GetDocumentsParams) => Promise<Array<Document<T>>>
294+
getDocuments: <P extends GetDocumentsParams<T>>(
295+
options?: P
296+
) => Promise<GetDocumentsResponse<T, P>>
267297
getDocument: (documentId: string | number) => Promise<Document<T>>
268298
addDocuments: (
269299
documents: Array<Document<T>>,
@@ -326,11 +356,11 @@ export interface MeiliAxiosWrapperInterface {
326356
data: IndexRequest,
327357
config?: AxiosRequestConfig
328358
) => Promise<Index<T>>) &
329-
(<T = any, R = AxiosResponse<EnqueuedUpdate>>(
330-
url: string,
331-
data?: T,
332-
config?: AxiosRequestConfig
333-
) => Promise<R>)
359+
(<T = any, R = AxiosResponse<EnqueuedUpdate>>(
360+
url: string,
361+
data?: T,
362+
config?: AxiosRequestConfig
363+
) => Promise<R>)
334364
put: <T = any, R = AxiosResponse<T>>(
335365
url: string,
336366
data?: any,

tests/search_tests.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe.each([
8888
await client
8989
.getIndex(index.uid)
9090
.search('prince')
91-
.then((response: Types.SearchResponse) => {
91+
.then((response) => {
9292
expect(response).toHaveProperty('hits', expect.any(Array))
9393
expect(response).toHaveProperty('offset', 0)
9494
expect(response).toHaveProperty('limit', 20)
@@ -102,7 +102,7 @@ describe.each([
102102
await client
103103
.getIndex(index.uid)
104104
.search('prince', { limit: 1 })
105-
.then((response: Types.SearchResponse) => {
105+
.then((response) => {
106106
expect(response).toHaveProperty('hits', expect.any(Array))
107107
expect(response).toHaveProperty('offset', 0)
108108
expect(response).toHaveProperty('limit', 1)
@@ -116,7 +116,7 @@ describe.each([
116116
await client
117117
.getIndex(index.uid)
118118
.search('prince', { limit: 1 })
119-
.then((response: Types.SearchResponse) => {
119+
.then((response) => {
120120
expect(response).toHaveProperty('hits', expect.any(Array))
121121
expect(response).toHaveProperty('offset', 0)
122122
expect(response).toHaveProperty('limit', 1)
@@ -132,7 +132,7 @@ describe.each([
132132
limit: 1,
133133
offset: 1,
134134
})
135-
.then((response: Types.SearchResponse) => {
135+
.then((response) => {
136136
expect(response).toHaveProperty('hits', [
137137
{
138138
id: 4,
@@ -157,7 +157,7 @@ describe.each([
157157
cropLength: 5,
158158
matches: true,
159159
})
160-
.then((response: Types.SearchResponse) => {
160+
.then((response) => {
161161
expect(response).toHaveProperty('hits', expect.any(Array))
162162
expect(response).toHaveProperty('offset', 0)
163163
expect(response).toHaveProperty('limit', 20)
@@ -184,7 +184,7 @@ describe.each([
184184
filters: 'title = "Le Petit Prince"',
185185
matches: true,
186186
})
187-
.then((response: Types.SearchResponse) => {
187+
.then((response) => {
188188
expect(response).toHaveProperty('hits', expect.any(Array))
189189
expect(response).toHaveProperty('offset', 0)
190190
expect(response).toHaveProperty('limit', 5)
@@ -225,7 +225,7 @@ describe.each([
225225
filters: 'title = "Le Petit Prince"',
226226
matches: true,
227227
})
228-
.then((response: Types.SearchResponse) => {
228+
.then((response) => {
229229
expect(response).toHaveProperty('hits', expect.any(Array))
230230
expect(response).toHaveProperty('offset', 0)
231231
expect(response).toHaveProperty('limit', 5)
@@ -260,7 +260,7 @@ describe.each([
260260
filters: 'title = "Le Petit Prince"',
261261
matches: true,
262262
})
263-
.then((response: Types.SearchResponse) => {
263+
.then((response) => {
264264
expect(response).toHaveProperty('hits', expect.any(Array))
265265
expect(response).toHaveProperty('offset', 0)
266266
expect(response).toHaveProperty('limit', 5)
@@ -297,9 +297,9 @@ describe.each([
297297
facetFilters: ['genre:romance'],
298298
facetsDistribution: ['genre'],
299299
})
300-
.then((response: Types.SearchResponse) => {
300+
.then((response) => {
301301
expect(response).toHaveProperty('facetsDistribution', {
302-
genre: { adventure: 0, fantasy: 0, romance: 2, "sci fi": 0, },
302+
genre: { adventure: 0, fantasy: 0, romance: 2, 'sci fi': 0 },
303303
})
304304
expect(response).toHaveProperty('exhaustiveFacetsCount', true)
305305
expect(response).toHaveProperty('hits', expect.any(Array))
@@ -313,7 +313,7 @@ describe.each([
313313
.search('h', {
314314
facetFilters: ['genre:sci fi'],
315315
})
316-
.then((response: Types.SearchResponse) => {
316+
.then((response) => {
317317
expect(response).toHaveProperty('hits', expect.any(Array))
318318
expect(response.hits.length).toEqual(1)
319319
})
@@ -326,9 +326,9 @@ describe.each([
326326
facetFilters: ['genre:romance', ['genre:romance', 'genre:romance']],
327327
facetsDistribution: ['genre'],
328328
})
329-
.then((response: Types.SearchResponse) => {
329+
.then((response) => {
330330
expect(response).toHaveProperty('facetsDistribution', {
331-
genre: { adventure: 0, fantasy: 0, romance: 2, "sci fi": 0, },
331+
genre: { adventure: 0, fantasy: 0, romance: 2, 'sci fi': 0 },
332332
})
333333
expect(response).toHaveProperty('exhaustiveFacetsCount', true)
334334
expect(response).toHaveProperty('hits', expect.any(Array))
@@ -340,7 +340,7 @@ describe.each([
340340
await client
341341
.getIndex(emptyIndex.uid)
342342
.search('prince')
343-
.then((response: Types.SearchResponse) => {
343+
.then((response) => {
344344
expect(response).toHaveProperty('hits', [])
345345
expect(response).toHaveProperty('offset', 0)
346346
expect(response).toHaveProperty('limit', 20)

0 commit comments

Comments
 (0)