Skip to content

Commit 30e833f

Browse files
authored
Add capabilities to update documents by function (#1691)
* Add types and function for updateDocumentsByFunction * Add tests * Fixed test * Fixed tests * Add param jsdoc * Fix style * Fix test, make tests more consistent
1 parent a2d46d5 commit 30e833f

File tree

4 files changed

+134
-3
lines changed

4 files changed

+134
-3
lines changed

src/indexes.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
SearchCutoffMs,
5656
SearchSimilarDocumentsParams,
5757
LocalizedAttributes,
58+
UpdateDocumentsByFunctionOptions,
5859
} from './types';
5960
import { removeUndefinedFromObject } from './utils';
6061
import { HttpRequests } from './http-requests';
@@ -630,6 +631,27 @@ class Index<T extends Record<string, any> = Record<string, any>> {
630631
return task;
631632
}
632633

634+
/**
635+
* This is an EXPERIMENTAL feature, which may break without a major version.
636+
* It's available after Meilisearch v1.10.
637+
*
638+
* More info about the feature:
639+
* https://github.com/orgs/meilisearch/discussions/762 More info about
640+
* experimental features in general:
641+
* https://www.meilisearch.com/docs/reference/api/experimental-features
642+
*
643+
* @param options - Object containing the function string and related options
644+
* @returns Promise containing an EnqueuedTask
645+
*/
646+
async updateDocumentsByFunction(
647+
options: UpdateDocumentsByFunctionOptions,
648+
): Promise<EnqueuedTask> {
649+
const url = `indexes/${this.uid}/documents/edit`;
650+
const task = await this.httpRequest.post(url, options);
651+
652+
return new EnqueuedTask(task);
653+
}
654+
633655
///
634656
/// SETTINGS
635657
///

src/types/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ export type DocumentsDeletionQuery = {
331331

332332
export type DocumentsIds = string[] | number[];
333333

334+
export type UpdateDocumentsByFunctionOptions = {
335+
function: string;
336+
filter?: string | string[];
337+
context?: Record<string, any>;
338+
};
339+
334340
/*
335341
** Settings
336342
*/

tests/documents.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,12 @@ describe('Documents tests', () => {
144144

145145
test(`${permission} key: Get documents with filters`, async () => {
146146
const client = await getClient(permission);
147-
await client.index(indexPk.uid).updateFilterableAttributes(['id']);
147+
148+
const { taskUid: updateFilterableAttributesTaskUid } = await client
149+
.index(indexPk.uid)
150+
.updateFilterableAttributes(['id']);
151+
await client.waitForTask(updateFilterableAttributesTaskUid);
152+
148153
const { taskUid } = await client
149154
.index(indexPk.uid)
150155
.addDocuments(dataset);
@@ -780,6 +785,42 @@ Hint: It might not be working because maybe you're not up to date with the Meili
780785
expect(index.primaryKey).toEqual(null);
781786
expect(task.status).toEqual('failed');
782787
});
788+
789+
test(`${permission} key: test updateDocumentsByFunction`, async () => {
790+
const client = await getClient(permission);
791+
const index = client.index<(typeof dataset)[number]>(indexPk.uid);
792+
const adminKey = await getKey('Admin');
793+
794+
const { taskUid: updateFilterableAttributesTaskUid } =
795+
await index.updateFilterableAttributes(['id']);
796+
await client.waitForTask(updateFilterableAttributesTaskUid);
797+
798+
await fetch(`${HOST}/experimental-features`, {
799+
body: JSON.stringify({ editDocumentsByFunction: true }),
800+
headers: {
801+
Authorization: `Bearer ${adminKey}`,
802+
'Content-Type': 'application/json',
803+
},
804+
method: 'PATCH',
805+
});
806+
807+
const { taskUid: addDocumentsTaskUid } =
808+
await index.addDocuments(dataset);
809+
await index.waitForTask(addDocumentsTaskUid);
810+
811+
const { taskUid: updateDocumentsByFunctionTaskUid } =
812+
await index.updateDocumentsByFunction({
813+
context: { ctx: 'Harry' },
814+
filter: 'id = 4',
815+
function: 'doc.comment = `Yer a wizard, ${context.ctx}!`',
816+
});
817+
818+
await client.waitForTask(updateDocumentsByFunctionTaskUid);
819+
820+
const doc = await index.getDocument(4);
821+
822+
expect(doc).toHaveProperty('comment', 'Yer a wizard, Harry!');
823+
});
783824
},
784825
);
785826

@@ -831,6 +872,24 @@ Hint: It might not be working because maybe you're not up to date with the Meili
831872
client.index(indexPk.uid).deleteAllDocuments(),
832873
).rejects.toHaveProperty('cause.code', ErrorStatusCode.INVALID_API_KEY);
833874
});
875+
876+
test(`${permission} key: Try updateDocumentsByFunction and be denied`, async () => {
877+
const client = await getClient(permission);
878+
const adminKey = await getKey('Admin');
879+
880+
await fetch(`${HOST}/experimental-features`, {
881+
body: JSON.stringify({ editDocumentsByFunction: true }),
882+
headers: {
883+
Authorization: `Bearer ${adminKey}`,
884+
'Content-Type': 'application/json',
885+
},
886+
method: 'PATCH',
887+
});
888+
889+
await expect(
890+
client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }),
891+
).rejects.toHaveProperty('cause.code', ErrorStatusCode.INVALID_API_KEY);
892+
});
834893
},
835894
);
836895

@@ -900,6 +959,27 @@ Hint: It might not be working because maybe you're not up to date with the Meili
900959
ErrorStatusCode.MISSING_AUTHORIZATION_HEADER,
901960
);
902961
});
962+
963+
test(`${permission} key: Try updateDocumentsByFunction and be denied`, async () => {
964+
const client = await getClient(permission);
965+
const adminKey = await getKey('Admin');
966+
967+
await fetch(`${HOST}/experimental-features`, {
968+
body: JSON.stringify({ editDocumentsByFunction: true }),
969+
headers: {
970+
Authorization: `Bearer ${adminKey}`,
971+
'Content-Type': 'application/json',
972+
},
973+
method: 'PATCH',
974+
});
975+
976+
await expect(
977+
client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }),
978+
).rejects.toHaveProperty(
979+
'cause.code',
980+
ErrorStatusCode.MISSING_AUTHORIZATION_HEADER,
981+
);
982+
});
903983
},
904984
);
905985

@@ -991,5 +1071,28 @@ Hint: It might not be working because maybe you're not up to date with the Meili
9911071
`Request to ${strippedHost}/${route} has failed`,
9921072
);
9931073
});
1074+
1075+
test(`Test updateDocumentsByFunction route`, async () => {
1076+
const route = `indexes/${indexPk.uid}/documents/edit`;
1077+
const client = new MeiliSearch({ host });
1078+
const strippedHost = trailing ? host.slice(0, -1) : host;
1079+
const adminKey = await getKey('Admin');
1080+
1081+
await fetch(`${HOST}/experimental-features`, {
1082+
body: JSON.stringify({ editDocumentsByFunction: true }),
1083+
headers: {
1084+
Authorization: `Bearer ${adminKey}`,
1085+
'Content-Type': 'application/json',
1086+
},
1087+
method: 'PATCH',
1088+
});
1089+
1090+
await expect(
1091+
client.index(indexPk.uid).updateDocumentsByFunction({ function: '' }),
1092+
).rejects.toHaveProperty(
1093+
'message',
1094+
`Request to ${strippedHost}/${route} has failed`,
1095+
);
1096+
});
9941097
});
9951098
});

tests/utils/meilisearch-test-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const clearAllIndexes = async (config: Config): Promise<void> => {
8080
const { results } = await client.getRawIndexes();
8181
const indexes = results.map((elem) => elem.uid);
8282

83-
const taskIds = [];
83+
const taskIds: number[] = [];
8484
for (const indexUid of indexes) {
8585
const { taskUid } = await client.index(indexUid).delete();
8686
taskIds.push(taskUid);
@@ -144,7 +144,7 @@ const datasetWithNests = [
144144
{ id: 7, title: "The Hitchhiker's Guide to the Galaxy" },
145145
];
146146

147-
const dataset = [
147+
const dataset: Array<{ id: number; title: string; comment?: string }> = [
148148
{ id: 123, title: 'Pride and Prejudice', comment: 'A great book' },
149149
{ id: 456, title: 'Le Petit Prince', comment: 'A french book' },
150150
{ id: 2, title: 'Le Rouge et le Noir', comment: 'Another french book' },

0 commit comments

Comments
 (0)