Skip to content

Commit

Permalink
Add capabilities to update documents by function (#1691)
Browse files Browse the repository at this point in the history
* Add types and function for updateDocumentsByFunction

* Add tests

* Fixed test

* Fixed tests

* Add param jsdoc

* Fix style

* Fix test, make tests more consistent
  • Loading branch information
flevi29 authored Aug 21, 2024
1 parent a2d46d5 commit 30e833f
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 3 deletions.
22 changes: 22 additions & 0 deletions src/indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
SearchCutoffMs,
SearchSimilarDocumentsParams,
LocalizedAttributes,
UpdateDocumentsByFunctionOptions,
} from './types';
import { removeUndefinedFromObject } from './utils';
import { HttpRequests } from './http-requests';
Expand Down Expand Up @@ -630,6 +631,27 @@ class Index<T extends Record<string, any> = Record<string, any>> {
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<EnqueuedTask> {
const url = `indexes/${this.uid}/documents/edit`;
const task = await this.httpRequest.post(url, options);

return new EnqueuedTask(task);
}

///
/// SETTINGS
///
Expand Down
6 changes: 6 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ export type DocumentsDeletionQuery = {

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

export type UpdateDocumentsByFunctionOptions = {
function: string;
filter?: string | string[];
context?: Record<string, any>;
};

/*
** Settings
*/
Expand Down
105 changes: 104 additions & 1 deletion tests/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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!');
});
},
);

Expand Down Expand Up @@ -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);
});
},
);

Expand Down Expand Up @@ -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,
);
});
},
);

Expand Down Expand Up @@ -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`,
);
});
});
});
4 changes: 2 additions & 2 deletions tests/utils/meilisearch-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const clearAllIndexes = async (config: Config): Promise<void> => {
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);
Expand Down Expand Up @@ -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' },
Expand Down

0 comments on commit 30e833f

Please sign in to comment.