diff --git a/src/indexes.ts b/src/indexes.ts index 048812d27..af7f36f78 100644 --- a/src/indexes.ts +++ b/src/indexes.ts @@ -55,6 +55,7 @@ import { SearchCutoffMs, SearchSimilarDocumentsParams, LocalizedAttributes, + UpdateDocumentsByFunctionOptions, } from './types'; import { removeUndefinedFromObject } from './utils'; import { HttpRequests } from './http-requests'; @@ -630,6 +631,27 @@ class Index = Record> { return task; } + /** + * This is an EXPERIMENTAL feature, which may break without a major version. + * It's available after Meilisearch v1.10. + * + * More info about the feature: + * https://github.com/orgs/meilisearch/discussions/762 More info about + * experimental features in general: + * https://www.meilisearch.com/docs/reference/api/experimental-features + * + * @param options - Object containing the function string and related options + * @returns Promise containing an EnqueuedTask + */ + async updateDocumentsByFunction( + options: UpdateDocumentsByFunctionOptions, + ): Promise { + const url = `indexes/${this.uid}/documents/edit`; + const task = await this.httpRequest.post(url, options); + + return new EnqueuedTask(task); + } + /// /// SETTINGS /// diff --git a/src/types/types.ts b/src/types/types.ts index 338af5d66..9c08bc4ad 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -331,6 +331,12 @@ export type DocumentsDeletionQuery = { export type DocumentsIds = string[] | number[]; +export type UpdateDocumentsByFunctionOptions = { + function: string; + filter?: string | string[]; + context?: Record; +}; + /* ** Settings */ diff --git a/tests/documents.test.ts b/tests/documents.test.ts index 8cb232e26..ac52be52c 100644 --- a/tests/documents.test.ts +++ b/tests/documents.test.ts @@ -144,7 +144,12 @@ describe('Documents tests', () => { test(`${permission} key: Get documents with filters`, async () => { const client = await getClient(permission); - await client.index(indexPk.uid).updateFilterableAttributes(['id']); + + const { taskUid: updateFilterableAttributesTaskUid } = await client + .index(indexPk.uid) + .updateFilterableAttributes(['id']); + await client.waitForTask(updateFilterableAttributesTaskUid); + const { taskUid } = await client .index(indexPk.uid) .addDocuments(dataset); @@ -780,6 +785,42 @@ Hint: It might not be working because maybe you're not up to date with the Meili expect(index.primaryKey).toEqual(null); expect(task.status).toEqual('failed'); }); + + test(`${permission} key: test updateDocumentsByFunction`, async () => { + const client = await getClient(permission); + const index = client.index<(typeof dataset)[number]>(indexPk.uid); + const adminKey = await getKey('Admin'); + + const { taskUid: updateFilterableAttributesTaskUid } = + await index.updateFilterableAttributes(['id']); + await client.waitForTask(updateFilterableAttributesTaskUid); + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ editDocumentsByFunction: true }), + headers: { + Authorization: `Bearer ${adminKey}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }); + + const { taskUid: addDocumentsTaskUid } = + await index.addDocuments(dataset); + await index.waitForTask(addDocumentsTaskUid); + + const { taskUid: updateDocumentsByFunctionTaskUid } = + await index.updateDocumentsByFunction({ + context: { ctx: 'Harry' }, + filter: 'id = 4', + function: 'doc.comment = `Yer a wizard, ${context.ctx}!`', + }); + + await client.waitForTask(updateDocumentsByFunctionTaskUid); + + const doc = await index.getDocument(4); + + expect(doc).toHaveProperty('comment', 'Yer a wizard, Harry!'); + }); }, ); @@ -831,6 +872,24 @@ Hint: It might not be working because maybe you're not up to date with the Meili client.index(indexPk.uid).deleteAllDocuments(), ).rejects.toHaveProperty('cause.code', ErrorStatusCode.INVALID_API_KEY); }); + + test(`${permission} key: Try updateDocumentsByFunction and be denied`, async () => { + const client = await getClient(permission); + const adminKey = await getKey('Admin'); + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ editDocumentsByFunction: true }), + headers: { + Authorization: `Bearer ${adminKey}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }); + + await expect( + client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }), + ).rejects.toHaveProperty('cause.code', ErrorStatusCode.INVALID_API_KEY); + }); }, ); @@ -900,6 +959,27 @@ Hint: It might not be working because maybe you're not up to date with the Meili ErrorStatusCode.MISSING_AUTHORIZATION_HEADER, ); }); + + test(`${permission} key: Try updateDocumentsByFunction and be denied`, async () => { + const client = await getClient(permission); + const adminKey = await getKey('Admin'); + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ editDocumentsByFunction: true }), + headers: { + Authorization: `Bearer ${adminKey}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }); + + await expect( + client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }), + ).rejects.toHaveProperty( + 'cause.code', + ErrorStatusCode.MISSING_AUTHORIZATION_HEADER, + ); + }); }, ); @@ -991,5 +1071,28 @@ Hint: It might not be working because maybe you're not up to date with the Meili `Request to ${strippedHost}/${route} has failed`, ); }); + + test(`Test updateDocumentsByFunction route`, async () => { + const route = `indexes/${indexPk.uid}/documents/edit`; + const client = new MeiliSearch({ host }); + const strippedHost = trailing ? host.slice(0, -1) : host; + const adminKey = await getKey('Admin'); + + await fetch(`${HOST}/experimental-features`, { + body: JSON.stringify({ editDocumentsByFunction: true }), + headers: { + Authorization: `Bearer ${adminKey}`, + 'Content-Type': 'application/json', + }, + method: 'PATCH', + }); + + await expect( + client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }), + ).rejects.toHaveProperty( + 'message', + `Request to ${strippedHost}/${route} has failed`, + ); + }); }); }); diff --git a/tests/utils/meilisearch-test-utils.ts b/tests/utils/meilisearch-test-utils.ts index d1313328c..e9d06c747 100644 --- a/tests/utils/meilisearch-test-utils.ts +++ b/tests/utils/meilisearch-test-utils.ts @@ -80,7 +80,7 @@ const clearAllIndexes = async (config: Config): Promise => { const { results } = await client.getRawIndexes(); const indexes = results.map((elem) => elem.uid); - const taskIds = []; + const taskIds: number[] = []; for (const indexUid of indexes) { const { taskUid } = await client.index(indexUid).delete(); taskIds.push(taskUid); @@ -144,7 +144,7 @@ const datasetWithNests = [ { id: 7, title: "The Hitchhiker's Guide to the Galaxy" }, ]; -const dataset = [ +const dataset: Array<{ id: number; title: string; comment?: string }> = [ { id: 123, title: 'Pride and Prejudice', comment: 'A great book' }, { id: 456, title: 'Le Petit Prince', comment: 'A french book' }, { id: 2, title: 'Le Rouge et le Noir', comment: 'Another french book' },