diff --git a/package.json b/package.json index 303f077d..28cbe09c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "meilisearch": "^0.42.0" }, "peerDependencies": { - "@strapi/strapi": "^4.0.0" + "@strapi/strapi": "^4.9.0" }, "author": { "name": "Charlotte Vermandel " diff --git a/server/__mocks__/meilisearch.js b/server/__mocks__/meilisearch.js index e93b9145..2a44cff8 100644 --- a/server/__mocks__/meilisearch.js +++ b/server/__mocks__/meilisearch.js @@ -4,6 +4,8 @@ const updateSettingsMock = jest.fn(() => 10) const deleteDocuments = jest.fn(() => { return [{ taskUid: 1 }, { taskUid: 2 }] }) +const deleteDocument = jest.fn(() => 3) + const getStats = jest.fn(() => { return { databaseSize: 447819776, @@ -41,6 +43,7 @@ const mockIndex = jest.fn(() => ({ addDocuments: addDocumentsMock, updateDocuments: updateDocumentsMock, updateSettings: updateSettingsMock, + deleteDocument, deleteDocuments, getStats: getIndexStats, })) diff --git a/server/__tests__/meilisearch.test.js b/server/__tests__/meilisearch.test.js index 737054b3..1924df8e 100644 --- a/server/__tests__/meilisearch.test.js +++ b/server/__tests__/meilisearch.test.js @@ -275,6 +275,106 @@ describe('Tests content types', () => { ]) }) + test('Test to update entries linked to multiple indexes in Meilisearch', async () => { + const pluginMock = jest.fn(() => ({ + // This rewrites only the needed methods to reach the system under test (removeSensitiveFields) + service: jest.fn().mockImplementation(() => { + return { + async actionInBatches({ contentType = 'restaurant', callback }) { + await callback({ + entries: [ + { + id: 1, + title: 'title', + internal_notes: 'note123', + secret: '123', + }, + { + id: 2, + title: 'abc', + internal_notes: 'note234', + secret: '234', + }, + ], + contentType, + }) + }, + getCollectionName: ({ contentType }) => contentType, + addIndexedContentType: jest.fn(), + subscribeContentType: jest.fn(), + getCredentials: () => ({}), + } + }), + })) + + // Spy + const client = new Meilisearch({ host: 'abc' }) + + const meilisearchService = createMeilisearchService({ + strapi: { + plugin: pluginMock, + contentTypes: { + restaurant: { + attributes: { + id: { private: false }, + title: { private: false }, + internal_notes: { private: true }, + secret: { private: true }, + }, + }, + }, + config: { + get: jest.fn(() => ({ + restaurant: { + noSanitizePrivateFields: ['internal_notes'], + indexName: ['customIndex', 'anotherIndex'], + }, + })), + }, + log: mockLogger, + }, + contentTypes: { + restaurant: { + attributes: { + id: { private: false }, + title: { private: false }, + internal_notes: { private: true }, + secret: { private: true }, + }, + }, + }, + }) + + const mockEntryUpdate = { attributes: { id: 1 } } + + const mockEntryCreate = { + _meilisearch_id: 'restaurant-1', + id: 3, + title: 'title', + internal_notes: 'note123', + publishedAt: null, + } + + const tasks = await meilisearchService.updateEntriesInMeilisearch({ + contentType: 'restaurant', + entries: [mockEntryUpdate, mockEntryCreate], + }) + + expect(strapi.log.info).toHaveBeenCalledTimes(4) + expect(strapi.log.info).toHaveBeenCalledWith( + 'A task to update 1 documents to the Meilisearch index "customIndex" has been enqueued.', + ) + expect(strapi.log.info).toHaveBeenCalledWith( + 'A task to update 1 documents to the Meilisearch index "anotherIndex" has been enqueued.', + ) + expect(client.index('').updateDocuments).toHaveBeenCalledTimes(2) + expect(client.index('').deleteDocument).toHaveBeenCalledTimes(2) + + expect(client.index).toHaveBeenCalledWith('customIndex') + expect(client.index).toHaveBeenCalledWith('anotherIndex') + expect(tasks).toEqual([3, 3, 10, 10]) + }) + test('Test to get stats', async () => { const customStrapi = createStrapiMock({}) diff --git a/server/services/lifecycle/lifecycle.js b/server/services/lifecycle/lifecycle.js index 7d0b6ac2..a8714337 100644 --- a/server/services/lifecycle/lifecycle.js +++ b/server/services/lifecycle/lifecycle.js @@ -42,10 +42,41 @@ module.exports = ({ strapi }) => { ) }) }, - async afterCreateMany() { - strapi.log.error( - `Meilisearch does not work with \`afterCreateMany\` hook as the entries are provided without their id`, - ) + async afterCreateMany(event) { + const { result } = event + const meilisearch = strapi + .plugin('meilisearch') + .service('meilisearch') + + const nbrEntries = result.count + const ids = result.ids + + const entries = [] + const BATCH_SIZE = 500 + for (let pos = 0; pos < nbrEntries; pos += BATCH_SIZE) { + const batch = await contentTypeService.getEntries({ + contentType: contentTypeUid, + start: pos, + limit: BATCH_SIZE, + filters: { + id: { + $in: ids, + }, + }, + }) + entries.push(...batch) + } + + meilisearch + .updateEntriesInMeilisearch({ + contentType: contentTypeUid, + entries: entries, + }) + .catch(e => { + strapi.log.error( + `Meilisearch could not update the entries: ${e.message}`, + ) + }) }, async afterUpdate(event) { const { result } = event diff --git a/server/services/meilisearch/connector.js b/server/services/meilisearch/connector.js index 597121da..f54af5b2 100644 --- a/server/services/meilisearch/connector.js +++ b/server/services/meilisearch/connector.js @@ -132,40 +132,58 @@ module.exports = ({ strapi, adapter, config }) => { if (!Array.isArray(entries)) entries = [entries] const indexUids = config.getIndexNamesOfContentType({ contentType }) - await Promise.all( + + const addDocuments = await sanitizeEntries({ + contentType, + entries, + config, + adapter, + }) + + // Check which documents are not in sanitized documents and need to be deleted + const deleteDocuments = entries.filter( + entry => !addDocuments.map(document => document.id).includes(entry.id), + ) + // Collect delete tasks + const deleteTasks = await Promise.all( indexUids.map(async indexUid => { const tasks = await Promise.all( - entries.map(async entry => { - const sanitized = await sanitizeEntries({ - entries: [entry], - contentType, - config, - adapter, - }) - - if (sanitized.length === 0) { - const task = await client.index(indexUid).deleteDocument( - adapter.addCollectionNamePrefixToId({ - contentType, - entryId: entry.id, - }), - ) - - strapi.log.info( - `A task to delete one document from the Meilisearch index "${indexUid}" has been enqueued (Task uid: ${task.taskUid}).`, - ) - - return task - } else { - return client - .index(indexUid) - .updateDocuments(sanitized, { primaryKey: '_meilisearch_id' }) - } + deleteDocuments.map(async document => { + const task = await client.index(indexUid).deleteDocument( + adapter.addCollectionNamePrefixToId({ + contentType, + entryId: document.id, + }), + ) + + strapi.log.info( + `A task to delete one document from the Meilisearch index "${indexUid}" has been enqueued (Task uid: ${task.taskUid}).`, + ) + + return task }), ) - return tasks.flat() + return tasks }), ) + + // Collect update tasks + const updateTasks = await Promise.all( + indexUids.map(async indexUid => { + const task = client.index(indexUid).updateDocuments(addDocuments, { + primaryKey: '_meilisearch_id', + }) + + strapi.log.info( + `A task to update ${addDocuments.length} documents to the Meilisearch index "${indexUid}" has been enqueued.`, + ) + + return task + }), + ) + + const tasks = [...deleteTasks.flat(), ...updateTasks] + return tasks }, /**