@vegan-friendly/strapi-plugin-elasticsearch 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegan-friendly/strapi-plugin-elasticsearch",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "A Strapi plugin to enable using Elasticsearch with Strapi CMS.",
5
5
  "homepage": "https://github.com/vegan-friendly/strapi-plugin-elasticsearch",
6
6
  "strapi": {
@@ -54,7 +54,7 @@
54
54
  }
55
55
  ],
56
56
  "engines": {
57
- "node": ">=16.0.0 <=20.x.x",
57
+ "node": ">=16.0.0",
58
58
  "npm": ">=6.0.0"
59
59
  },
60
60
  "license": "MIT",
@@ -34,6 +34,10 @@ declare const _default: {
34
34
  required: boolean;
35
35
  default: string;
36
36
  };
37
+ error_message: {
38
+ type: string;
39
+ required: boolean;
40
+ };
37
41
  full_site_indexing: {
38
42
  type: string;
39
43
  };
@@ -32,6 +32,10 @@ declare const _default: {
32
32
  required: boolean;
33
33
  default: string;
34
34
  };
35
+ error_message: {
36
+ type: string;
37
+ required: boolean;
38
+ };
35
39
  full_site_indexing: {
36
40
  type: string;
37
41
  };
@@ -14,7 +14,7 @@ exports.default = {
14
14
  },
15
15
  pluginOptions: {
16
16
  'content-manager': {
17
- visible: false,
17
+ visible: true,
18
18
  },
19
19
  'content-type-builder': {
20
20
  visible: false,
@@ -23,17 +23,21 @@ exports.default = {
23
23
  attributes: {
24
24
  collection_name: {
25
25
  type: 'string',
26
- required: true,
26
+ required: false,
27
27
  },
28
28
  item_id: {
29
29
  type: 'integer',
30
30
  },
31
31
  indexing_status: {
32
32
  type: 'enumeration',
33
- enum: ['to-be-done', 'done'],
33
+ enum: ['to-be-done', 'in-progress', 'done', 'failed'],
34
34
  required: true,
35
35
  default: 'to-be-done',
36
36
  },
37
+ error_message: {
38
+ type: 'string',
39
+ required: false,
40
+ },
37
41
  full_site_indexing: {
38
42
  type: 'boolean',
39
43
  },
@@ -142,7 +142,9 @@ declare const _default: {
142
142
  recordId: any;
143
143
  }): Promise<void>;
144
144
  getItemsPendingToBeIndexed(): Promise<any>;
145
- markIndexingTaskComplete(recId: any): Promise<void>;
145
+ markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
146
+ markIndexingTaskInProgress(recId: any): Promise<void>;
147
+ getFullIndexingInProgress(): Promise<any>;
146
148
  };
147
149
  esInterface: ({ strapi }: {
148
150
  strapi: any;
@@ -150,8 +152,8 @@ declare const _default: {
150
152
  indexer: ({ strapi }: {
151
153
  strapi: any;
152
154
  }) => {
153
- rebuildIndex(): Promise<boolean>;
154
- indexCollection(collectionName: any, indexName?: string | null): Promise<boolean>;
155
+ rebuildIndex(task?: any): Promise<boolean>;
156
+ indexCollection(collectionName: any, indexName?: string | null): Promise<number>;
155
157
  indexPendingData(): Promise<boolean>;
156
158
  };
157
159
  logIndexing: ({ strapi }: {
@@ -213,6 +215,10 @@ declare const _default: {
213
215
  required: boolean;
214
216
  default: string;
215
217
  };
218
+ error_message: {
219
+ type: string;
220
+ required: boolean;
221
+ };
216
222
  full_site_indexing: {
217
223
  type: string;
218
224
  };
@@ -38,7 +38,9 @@ declare const _default: {
38
38
  recordId: any;
39
39
  }): Promise<void>;
40
40
  getItemsPendingToBeIndexed(): Promise<any>;
41
- markIndexingTaskComplete(recId: any): Promise<void>;
41
+ markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
42
+ markIndexingTaskInProgress(recId: any): Promise<void>;
43
+ getFullIndexingInProgress(): Promise<any>;
42
44
  };
43
45
  esInterface: ({ strapi }: {
44
46
  strapi: any;
@@ -46,8 +48,8 @@ declare const _default: {
46
48
  indexer: ({ strapi }: {
47
49
  strapi: any;
48
50
  }) => {
49
- rebuildIndex(): Promise<boolean>;
50
- indexCollection(collectionName: any, indexName?: string | null): Promise<boolean>;
51
+ rebuildIndex(task?: any): Promise<boolean>;
52
+ indexCollection(collectionName: any, indexName?: string | null): Promise<number>;
51
53
  indexPendingData(): Promise<boolean>;
52
54
  };
53
55
  logIndexing: ({ strapi }: {
@@ -1,8 +1,8 @@
1
1
  declare const _default: ({ strapi }: {
2
2
  strapi: any;
3
3
  }) => {
4
- rebuildIndex(): Promise<boolean>;
5
- indexCollection(collectionName: any, indexName?: string | null): Promise<boolean>;
4
+ rebuildIndex(task?: any): Promise<boolean>;
5
+ indexCollection(collectionName: any, indexName?: string | null): Promise<number>;
6
6
  indexPendingData(): Promise<boolean>;
7
7
  };
8
8
  export default _default;
@@ -1,32 +1,54 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = ({ strapi }) => ({
4
- async rebuildIndex() {
4
+ async rebuildIndex(task = null) {
5
5
  const helper = strapi.plugins['elasticsearch'].services.helper;
6
6
  const esInterface = strapi.plugins['elasticsearch'].services.esInterface;
7
7
  const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing;
8
8
  const configureIndexingService = strapi.plugins['elasticsearch'].services.configureIndexing;
9
9
  const logIndexingService = strapi.plugins['elasticsearch'].services.logIndexing;
10
10
  const virtualCollectionsIndexer = strapi.plugins['elasticsearch'].services['virtualCollectionsIndexer'];
11
+ const virtualCollectionsRegistry = strapi.plugins['elasticsearch'].services['virtualCollectionsRegistry'];
12
+ let taskError = null;
11
13
  try {
12
14
  console.log('strapi-plugin-elasticsearch : Request to rebuild the index received.');
15
+ const fullIndexingInProgress = await scheduleIndexingService.getFullIndexingInProgress();
16
+ if (fullIndexingInProgress.length > 0) {
17
+ const msg = `Indexing is already in progress - see tasks ${fullIndexingInProgress.map((t) => t.id)}. This request is ignored and marked as failed.`;
18
+ console.log('strapi-plugin-elasticsearch : ' + msg);
19
+ await logIndexingService.recordIndexingFail(msg);
20
+ return false;
21
+ }
22
+ const cols = await configureIndexingService.getCollectionsConfiguredForIndexing();
23
+ const needsNewIndex = cols.length > 0 || virtualCollectionsRegistry.getAll().some((vc) => vc.indexAlias == null);
13
24
  const oldIndexName = await helper.getCurrentIndexName();
14
25
  console.log('strapi-plugin-elasticsearch : Recording the previous index name : ', oldIndexName);
15
26
  //Step 1 : Create a new index
16
- const newIndexName = await helper.getIncrementedIndexName();
17
- await esInterface.createIndex(newIndexName);
18
- console.log('strapi-plugin-elasticsearch : Created new index with name : ', newIndexName);
27
+ let newIndexName;
28
+ if (needsNewIndex) {
29
+ newIndexName = await helper.getIncrementedIndexName();
30
+ await esInterface.createIndex(newIndexName);
31
+ console.log('strapi-plugin-elasticsearch : Created new index with name : ', newIndexName);
32
+ }
33
+ else {
34
+ newIndexName = oldIndexName;
35
+ console.log('strapi-plugin-elasticsearch : No need to create new index, as there are no collections to re-index, and no virtual-collections that use the default index. sticking to current index:', newIndexName);
36
+ }
19
37
  //Step 2 : Index all the stuff on this new index
20
38
  console.log('strapi-plugin-elasticsearch : Starting to index all data into the new index.');
21
- const item = await scheduleIndexingService.addFullSiteIndexingTask();
22
- if (item.id) {
23
- const cols = await configureIndexingService.getCollectionsConfiguredForIndexing();
24
- for (let r = 0; r < cols.length; r++)
25
- await this.indexCollection(cols[r], newIndexName);
39
+ if (task == null) {
40
+ task = await scheduleIndexingService.addFullSiteIndexingTask();
41
+ }
42
+ if (task?.id) {
43
+ await scheduleIndexingService.markIndexingTaskInProgress(task.id);
44
+ let entitiesIndexed = 0;
45
+ for (let r = 0; r < cols.length; r++) {
46
+ entitiesIndexed += await this.indexCollection(cols[r], newIndexName);
47
+ }
26
48
  // Indexing the virtual collections
27
- console.log('strapi-plugin-elasticsearch : Starting to index virtual collections.');
28
- const totalIndexed = await virtualCollectionsIndexer.reindexAll(newIndexName);
29
- await scheduleIndexingService.markIndexingTaskComplete(item.id);
49
+ console.log('strapi-plugin-elasticsearch : Starting to index virtual collections. task id : ', task.id);
50
+ const virtualEntriesIndexed = await virtualCollectionsIndexer.reindexAll(newIndexName);
51
+ await scheduleIndexingService.markIndexingTaskComplete(task.id);
30
52
  console.log('strapi-plugin-elasticsearch : Indexing of data into the new index complete.');
31
53
  //Step 4 : Move the alias to this new index
32
54
  await esInterface.attachAliasToIndex(newIndexName);
@@ -34,7 +56,7 @@ exports.default = ({ strapi }) => ({
34
56
  console.log('strapi-plugin-elasticsearch : Deleting the previous indices');
35
57
  //Step 5 : Delete the previous index
36
58
  await helper.deleteOldIndices();
37
- await logIndexingService.recordIndexingPass('Request to immediately re-index site-wide content completed successfully.');
59
+ await logIndexingService.recordIndexingPass(`Re-index site-wide content completed successfully. ${entitiesIndexed} entries indexed. ${virtualEntriesIndexed} virtual entries indexed.`);
38
60
  return true;
39
61
  }
40
62
  else {
@@ -43,11 +65,17 @@ exports.default = ({ strapi }) => ({
43
65
  }
44
66
  }
45
67
  catch (err) {
68
+ taskError = err.message || String(err);
46
69
  console.log('strapi-plugin-elasticsearch : searchController : An error was encountered while re-indexing.');
47
70
  console.log(err);
48
71
  await logIndexingService.recordIndexingFail(err);
49
72
  throw err;
50
73
  }
74
+ finally {
75
+ if (task?.id) {
76
+ await scheduleIndexingService.markIndexingTaskComplete(task.id, taskError);
77
+ }
78
+ }
51
79
  },
52
80
  async indexCollection(collectionName, indexName = null) {
53
81
  const helper = strapi.plugins['elasticsearch'].services.helper;
@@ -93,7 +121,7 @@ exports.default = ({ strapi }) => ({
93
121
  await esInterface.indexDataToSpecificIndex({ itemId: indexItemId, itemData: dataToIndex }, indexName);
94
122
  }
95
123
  }
96
- return true;
124
+ return entries.length ?? 0;
97
125
  },
98
126
  async indexPendingData() {
99
127
  const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing;
@@ -103,17 +131,22 @@ exports.default = ({ strapi }) => ({
103
131
  const helper = strapi.plugins['elasticsearch'].services.helper;
104
132
  const indexAlias = await strapi.config.get('plugin.elasticsearch').indexAliasName;
105
133
  const recs = await scheduleIndexingService.getItemsPendingToBeIndexed();
106
- const fullSiteIndexing = recs.filter((r) => r.full_site_indexing === true).length > 0;
134
+ const fullSiteIndexTasks = recs.filter((r) => r.full_site_indexing === true);
135
+ const fullSiteIndexing = fullSiteIndexTasks.length > 0;
107
136
  if (fullSiteIndexing) {
108
- await this.rebuildIndex();
109
- for (let r = 0; r < recs.length; r++)
110
- await scheduleIndexingService.markIndexingTaskComplete(recs[r].id);
137
+ const success = await this.rebuildIndex(fullSiteIndexTasks[0]);
138
+ if (success) {
139
+ // Mark all pending tasks as complete, as they are implicitly covered by the full-site indexing.
140
+ for (let r = 0; r < recs.length; r++)
141
+ await scheduleIndexingService.markIndexingTaskComplete(recs[r].id);
142
+ }
111
143
  }
112
144
  else {
113
145
  try {
114
146
  for (let r = 0; r < recs.length; r++) {
115
147
  const col = recs[r].collection_name;
116
148
  if (configureIndexingService.isCollectionConfiguredToBeIndexed(col)) {
149
+ await scheduleIndexingService.markIndexingTaskInProgress(recs[r].id);
117
150
  //Indexing the individual item
118
151
  if (recs[r].item_id) {
119
152
  if (recs[r].indexing_type !== 'remove-from-index') {
@@ -14,6 +14,8 @@ declare const _default: ({ strapi }: {
14
14
  recordId: any;
15
15
  }): Promise<void>;
16
16
  getItemsPendingToBeIndexed(): Promise<any>;
17
- markIndexingTaskComplete(recId: any): Promise<void>;
17
+ markIndexingTaskComplete(recId: any, error?: string | null): Promise<void>;
18
+ markIndexingTaskInProgress(recId: any): Promise<void>;
19
+ getFullIndexingInProgress(): Promise<any>;
18
20
  };
19
21
  export default _default;
@@ -52,11 +52,29 @@ exports.default = ({ strapi }) => ({
52
52
  });
53
53
  return entries;
54
54
  },
55
- async markIndexingTaskComplete(recId) {
55
+ async markIndexingTaskComplete(recId, error = null) {
56
+ const status = error ? 'failed' : 'done';
56
57
  const entries = await strapi.entityService.update('plugin::elasticsearch.task', recId, {
57
58
  data: {
58
- indexing_status: 'done',
59
+ indexing_status: status,
60
+ error_message: error,
59
61
  },
60
62
  });
61
63
  },
64
+ async markIndexingTaskInProgress(recId) {
65
+ await strapi.entityService.update('plugin::elasticsearch.task', recId, {
66
+ data: {
67
+ indexing_status: 'in-progress',
68
+ },
69
+ });
70
+ },
71
+ async getFullIndexingInProgress() {
72
+ const entries = await strapi.entityService.findMany('plugin::elasticsearch.task', {
73
+ filters: {
74
+ indexing_status: 'in-progress',
75
+ full_site_indexing: true,
76
+ },
77
+ });
78
+ return entries;
79
+ },
62
80
  });
@@ -24,7 +24,9 @@ exports.default = ({ strapi }) => {
24
24
  try {
25
25
  const results = await collection.extractByIds([itemId]);
26
26
  if (!results || !Array.isArray(results) || results.length === 0) {
27
- strapi.log.warn(`No data extracted for ${collectionName} with ID ${itemId}`);
27
+ // item does not exit - delete it from index
28
+ await this.deleteItem(collectionName, itemId);
29
+ strapi.log.debug(`Deleted virtual item: ${collectionName}:${itemId}`);
28
30
  return null;
29
31
  }
30
32
  const itemData = results[0];
@@ -58,11 +60,13 @@ exports.default = ({ strapi }) => {
58
60
  async reindex(collection) {
59
61
  const collectionName = collection.collectionName;
60
62
  const privateIndexAlias = collection.indexAlias;
63
+ const pageSize = 100;
61
64
  const helper = getHelperService();
62
65
  let timestamp = Date.now();
66
+ let indexName = '';
67
+ let errors = 0;
63
68
  try {
64
69
  const esInterface = getElasticsearchService();
65
- let indexName;
66
70
  if (privateIndexAlias) {
67
71
  indexName = await helper.getIncrementedIndexName(privateIndexAlias);
68
72
  await esInterface.createIndex(indexName, collection.mappings);
@@ -71,25 +75,53 @@ exports.default = ({ strapi }) => {
71
75
  indexName = await helper.getCurrentIndexName();
72
76
  }
73
77
  let page = 0;
74
- let hasMoreData = true;
78
+ let prevPageData = [];
75
79
  let totalIndexed = 0;
76
- while (hasMoreData) {
77
- const pageData = await collection.extractData(page);
80
+ const pageLimit = 10000;
81
+ while (page <= pageLimit) {
82
+ let pageData;
83
+ try {
84
+ pageData = await collection.extractData(page, pageSize);
85
+ }
86
+ catch (error) {
87
+ strapi.log.error(`Error extracting data for page ${page} of ${collectionName}: ${error.message}`);
88
+ errors += pageSize;
89
+ page++;
90
+ continue;
91
+ }
92
+ strapi.log.debug(`Extracted ${pageData.length} items from ${collectionName} for page ${page}`);
78
93
  if (!Array.isArray(pageData) || pageData.length === 0) {
79
- hasMoreData = false;
80
94
  break;
81
95
  }
96
+ if (JSON.stringify(prevPageData) == JSON.stringify(pageData)) {
97
+ throw new Error(`Infinite loop detected at page ${page} while reindexing ${collectionName}. Stopping reindexing. Check this virtual-collection's extractData().
98
+ current page 1st item (id ${pageData[0]?.id}):
99
+ ${JSON.stringify(pageData[0])}
100
+ prev page 1st item (id ${prevPageData[0]?.id}):
101
+ ${JSON.stringify(prevPageData[0])}`);
102
+ }
103
+ if (page >= pageLimit) {
104
+ strapi.log.warn(`Page ${page} of ${collectionName} is greater than page-limit (${pageLimit}). stopping indexing this virtual-collection.`);
105
+ pageData.length = pageLimit;
106
+ }
82
107
  const operations = [];
83
108
  for (const itemData of pageData) {
84
109
  const itemId = collection.getIndexItemId(itemData.id, collectionName);
85
110
  operations.push({ itemId, itemData });
86
111
  }
87
112
  if (operations.length > 0) {
88
- await Promise.all(operations.map((op) => esInterface.indexDataToSpecificIndex(op, indexName)));
113
+ await Promise.all(operations.map((op) => esInterface.indexDataToSpecificIndex(op, indexName).catch((err) => {
114
+ strapi.log.error(`Failed to index item ${op.itemId} in ${collectionName}: ${err}`);
115
+ errors++;
116
+ })));
89
117
  }
90
118
  totalIndexed += pageData.length;
119
+ prevPageData = pageData;
91
120
  page++;
92
121
  }
122
+ if (errors > 0) {
123
+ throw new Error(`Failed to index ${errors} of ${totalIndexed} items for virtual collection ${collectionName}. Errors were logged. Alias was not updated. took ${(0, humanize_duration_1.default)(Date.now() - timestamp)}`);
124
+ }
93
125
  strapi.log.info(`Reindexed ${totalIndexed} items for virtual collection: ${collectionName}. took ${(0, humanize_duration_1.default)(Date.now() - timestamp)}. now updating alias.`);
94
126
  if (privateIndexAlias) {
95
127
  timestamp = Date.now();
@@ -102,7 +134,7 @@ exports.default = ({ strapi }) => {
102
134
  return totalIndexed;
103
135
  }
104
136
  catch (error) {
105
- strapi.log.error(`Error reindexing ${collectionName}: ${error?.message} after ${(0, humanize_duration_1.default)(Date.now() - timestamp)}`);
137
+ strapi.log.error(`Error reindexing ${collectionName} to index ${indexName}: ${error?.message} after ${(0, humanize_duration_1.default)(Date.now() - timestamp)}`);
106
138
  throw error;
107
139
  }
108
140
  },
@@ -125,14 +157,7 @@ exports.default = ({ strapi }) => {
125
157
  const idsToReindex = await trigger.getIdsToReindex(result);
126
158
  // Reindex each item
127
159
  for (const id of idsToReindex) {
128
- const isDelete = event.action?.toLowerCase()?.includes('delete') && trigger.alsoTriggerDelete && id === result.id;
129
- if (isDelete) {
130
- //delete the item from the index, if the item being delete is the one being reindexed
131
- await this.deleteItem(collection.collectionName, id);
132
- }
133
- else {
134
- await this.indexItem(collection.collectionName, id);
135
- }
160
+ await this.indexItem(collection.collectionName, id);
136
161
  }
137
162
  }
138
163
  },
@@ -47,10 +47,9 @@ const configSchema = yup.object({
47
47
  .of(yup.object({
48
48
  collection: yup.string().required(),
49
49
  getIdsToReindex: isFunction().required(),
50
- alsoTriggerDelete: yup.boolean().default(false),
51
50
  }))
52
51
  .default([]),
53
- mappings: yup.object().default({}),
52
+ mappings: yup.object().optional(),
54
53
  });
55
54
  /**
56
55
  * Service to handle indexing of virtual collections
@@ -73,7 +72,7 @@ exports.default = ({ strapi }) => {
73
72
  const virtualCollectionsFactories = strapi.plugin('elasticsearch').config('virtualCollections') || [];
74
73
  config = virtualCollectionsFactories.map((factory) => {
75
74
  let collectionConfig = factory(strapi);
76
- collectionConfig = configSchema.validateSync(collectionConfig, { stripUnknown: true });
75
+ collectionConfig = configSchema.validateSync(collectionConfig);
77
76
  collectionConfig.getIndexItemId = collectionConfig.getIndexItemId || ((id) => helper.getIndexItemId({ collectionName: collectionConfig.collectionName, itemId: id }));
78
77
  return { ...defaultConf, ...collectionConfig };
79
78
  });
@@ -45,11 +45,6 @@ export type VirtualCollectionConfig = {
45
45
  * @returns ids of the items to be reindexed.
46
46
  */
47
47
  getIdsToReindex: GetIdsToIndexFunction;
48
- /**
49
- * if true, and the trigger is a delete event, the item of the virtual collection will be deleted as well if the id returned from getIdsToReindex match.
50
- * defaults to false.
51
- */
52
- alsoTriggerDelete?: boolean;
53
48
  }>;
54
49
  /**
55
50
  * Optional schema to be sent to Elasticsearch when creating the index.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegan-friendly/strapi-plugin-elasticsearch",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "A Strapi plugin to enable using Elasticsearch with Strapi CMS.",
5
5
  "homepage": "https://github.com/vegan-friendly/strapi-plugin-elasticsearch",
6
6
  "strapi": {
@@ -54,7 +54,7 @@
54
54
  }
55
55
  ],
56
56
  "engines": {
57
- "node": ">=16.0.0 <=20.x.x",
57
+ "node": ">=16.0.0",
58
58
  "npm": ">=6.0.0"
59
59
  },
60
60
  "license": "MIT",
@@ -64,4 +64,4 @@
64
64
  "ts-node": "^10.9.2",
65
65
  "typescript": "^5.8.3"
66
66
  }
67
- }
67
+ }