@vegan-friendly/strapi-plugin-elasticsearch 0.2.7 → 0.2.9

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.7",
3
+ "version": "0.2.9",
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": {
@@ -25,7 +25,12 @@ exports.default = async ({ strapi }) => {
25
25
  strapi.cron.add({
26
26
  elasticsearchIndexing: {
27
27
  task: async ({ strapi }) => {
28
- await indexer.indexPendingData();
28
+ try {
29
+ await indexer.indexPendingData();
30
+ }
31
+ catch (err) {
32
+ strapi.log.error('Error while indexing data: ', err);
33
+ }
29
34
  },
30
35
  options: {
31
36
  rule: pluginConfig['indexingCronSchedule'],
@@ -119,6 +124,15 @@ exports.default = async ({ strapi }) => {
119
124
  const registry = strapi.service('plugin::elasticsearch.virtualCollectionsRegistry');
120
125
  // Setup lifecycle hooks
121
126
  const virtualCollections = registry.getAll();
127
+ // Check if indices exists, if not create them
128
+ virtualCollections.forEach(async (collection) => {
129
+ const indexName = await helper.getCurrentIndexName(collection.indexAlias);
130
+ const indexExists = await esInterface.listIndicesByPattern(indexName);
131
+ if (!indexExists.includes(indexName)) {
132
+ await esInterface.createIndex(indexName, collection.mappings);
133
+ strapi.log.info(`Created Elasticsearch index: ${indexName}`);
134
+ }
135
+ });
122
136
  // Create a set of all collections that need hooks
123
137
  const collectionsToHook = new Set();
124
138
  virtualCollections.forEach((collection) => {
@@ -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,14 +23,14 @@ 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'],
34
34
  required: true,
35
35
  default: 'to-be-done',
36
36
  },
@@ -143,6 +143,8 @@ declare const _default: {
143
143
  }): Promise<void>;
144
144
  getItemsPendingToBeIndexed(): Promise<any>;
145
145
  markIndexingTaskComplete(recId: any): 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(item?: any): Promise<boolean>;
156
+ indexCollection(collectionName: any, indexName?: string | null): Promise<number>;
155
157
  indexPendingData(): Promise<boolean>;
156
158
  };
157
159
  logIndexing: ({ strapi }: {
@@ -156,10 +156,18 @@ exports.default = ({ strapi }) => ({
156
156
  }
157
157
  },
158
158
  async listIndicesByPattern(pattern = 'restaurants*') {
159
- const results = await client.cat.indices({
160
- index: pattern,
161
- format: 'json',
162
- });
163
- return results.map((index) => index.index).filter((index) => index != null);
159
+ try {
160
+ const results = await client.cat.indices({
161
+ index: pattern,
162
+ format: 'json',
163
+ });
164
+ return results.map((index) => index.index).filter((index) => index != null);
165
+ }
166
+ catch (err) {
167
+ if (err?.message?.includes('index_not_found_exception')) {
168
+ return [];
169
+ }
170
+ throw err;
171
+ }
164
172
  },
165
173
  });
@@ -39,6 +39,8 @@ declare const _default: {
39
39
  }): Promise<void>;
40
40
  getItemsPendingToBeIndexed(): Promise<any>;
41
41
  markIndexingTaskComplete(recId: any): 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(item?: 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(item?: 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,31 +1,52 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = ({ strapi }) => ({
4
- async rebuildIndex() {
4
+ async rebuildIndex(item = 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'];
11
12
  try {
12
13
  console.log('strapi-plugin-elasticsearch : Request to rebuild the index received.');
14
+ const fullIndexingInProgress = await scheduleIndexingService.getFullIndexingInProgress();
15
+ if (fullIndexingInProgress.length > 0) {
16
+ const msg = `Indexing is already in progress - see tasks ${fullIndexingInProgress.map((t) => t.id)}. This request is ignored and marked as failed.`;
17
+ console.log('strapi-plugin-elasticsearch : ' + msg);
18
+ await logIndexingService.recordIndexingFail(msg);
19
+ return false;
20
+ }
21
+ const cols = await configureIndexingService.getCollectionsConfiguredForIndexing();
22
+ const needsNewIndex = cols.length > 0 || virtualCollectionsRegistry.getAll().some((vc) => vc.indexAlias == null);
13
23
  const oldIndexName = await helper.getCurrentIndexName();
14
24
  console.log('strapi-plugin-elasticsearch : Recording the previous index name : ', oldIndexName);
15
25
  //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);
26
+ let newIndexName;
27
+ if (needsNewIndex) {
28
+ newIndexName = await helper.getIncrementedIndexName();
29
+ await esInterface.createIndex(newIndexName);
30
+ console.log('strapi-plugin-elasticsearch : Created new index with name : ', newIndexName);
31
+ }
32
+ else {
33
+ newIndexName = oldIndexName;
34
+ 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);
35
+ }
19
36
  //Step 2 : Index all the stuff on this new index
20
37
  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);
38
+ if (item == null) {
39
+ item = await scheduleIndexingService.addFullSiteIndexingTask();
40
+ }
41
+ if (item?.id) {
42
+ await scheduleIndexingService.markIndexingTaskInProgress(item.id);
43
+ let entitiesIndexed = 0;
44
+ for (let r = 0; r < cols.length; r++) {
45
+ entitiesIndexed += await this.indexCollection(cols[r], newIndexName);
46
+ }
26
47
  // Indexing the virtual collections
27
- console.log('strapi-plugin-elasticsearch : Starting to index virtual collections.');
28
- const totalIndexed = await virtualCollectionsIndexer.reindexAll(newIndexName);
48
+ console.log('strapi-plugin-elasticsearch : Starting to index virtual collections. task id : ', item.id);
49
+ const virtualEntriesIndexed = await virtualCollectionsIndexer.reindexAll(newIndexName);
29
50
  await scheduleIndexingService.markIndexingTaskComplete(item.id);
30
51
  console.log('strapi-plugin-elasticsearch : Indexing of data into the new index complete.');
31
52
  //Step 4 : Move the alias to this new index
@@ -34,7 +55,7 @@ exports.default = ({ strapi }) => ({
34
55
  console.log('strapi-plugin-elasticsearch : Deleting the previous indices');
35
56
  //Step 5 : Delete the previous index
36
57
  await helper.deleteOldIndices();
37
- await logIndexingService.recordIndexingPass('Request to immediately re-index site-wide content completed successfully.');
58
+ await logIndexingService.recordIndexingPass(`Re-index site-wide content completed successfully. ${entitiesIndexed} entries indexed. ${virtualEntriesIndexed} virtual entries indexed.`);
38
59
  return true;
39
60
  }
40
61
  else {
@@ -48,6 +69,11 @@ exports.default = ({ strapi }) => ({
48
69
  await logIndexingService.recordIndexingFail(err);
49
70
  throw err;
50
71
  }
72
+ finally {
73
+ if (item?.id) {
74
+ await scheduleIndexingService.markIndexingTaskComplete(item.id);
75
+ }
76
+ }
51
77
  },
52
78
  async indexCollection(collectionName, indexName = null) {
53
79
  const helper = strapi.plugins['elasticsearch'].services.helper;
@@ -93,7 +119,7 @@ exports.default = ({ strapi }) => ({
93
119
  await esInterface.indexDataToSpecificIndex({ itemId: indexItemId, itemData: dataToIndex }, indexName);
94
120
  }
95
121
  }
96
- return true;
122
+ return entries.length ?? 0;
97
123
  },
98
124
  async indexPendingData() {
99
125
  const scheduleIndexingService = strapi.plugins['elasticsearch'].services.scheduleIndexing;
@@ -103,17 +129,22 @@ exports.default = ({ strapi }) => ({
103
129
  const helper = strapi.plugins['elasticsearch'].services.helper;
104
130
  const indexAlias = await strapi.config.get('plugin.elasticsearch').indexAliasName;
105
131
  const recs = await scheduleIndexingService.getItemsPendingToBeIndexed();
106
- const fullSiteIndexing = recs.filter((r) => r.full_site_indexing === true).length > 0;
132
+ const fullSiteIndexTasks = recs.filter((r) => r.full_site_indexing === true);
133
+ const fullSiteIndexing = fullSiteIndexTasks.length > 0;
107
134
  if (fullSiteIndexing) {
108
- await this.rebuildIndex();
109
- for (let r = 0; r < recs.length; r++)
110
- await scheduleIndexingService.markIndexingTaskComplete(recs[r].id);
135
+ const success = await this.rebuildIndex(fullSiteIndexTasks[0]);
136
+ if (success) {
137
+ // Mark all pending tasks as complete, as they are implicitly covered by the full-site indexing.
138
+ for (let r = 0; r < recs.length; r++)
139
+ await scheduleIndexingService.markIndexingTaskComplete(recs[r].id);
140
+ }
111
141
  }
112
142
  else {
113
143
  try {
114
144
  for (let r = 0; r < recs.length; r++) {
115
145
  const col = recs[r].collection_name;
116
146
  if (configureIndexingService.isCollectionConfiguredToBeIndexed(col)) {
147
+ await scheduleIndexingService.markIndexingTaskInProgress(recs[r].id);
117
148
  //Indexing the individual item
118
149
  if (recs[r].item_id) {
119
150
  if (recs[r].indexing_type !== 'remove-from-index') {
@@ -15,5 +15,7 @@ declare const _default: ({ strapi }: {
15
15
  }): Promise<void>;
16
16
  getItemsPendingToBeIndexed(): Promise<any>;
17
17
  markIndexingTaskComplete(recId: any): Promise<void>;
18
+ markIndexingTaskInProgress(recId: any): Promise<void>;
19
+ getFullIndexingInProgress(): Promise<any>;
18
20
  };
19
21
  export default _default;
@@ -59,4 +59,20 @@ exports.default = ({ strapi }) => ({
59
59
  },
60
60
  });
61
61
  },
62
+ async markIndexingTaskInProgress(recId) {
63
+ await strapi.entityService.update('plugin::elasticsearch.task', recId, {
64
+ data: {
65
+ indexing_status: 'in-progress',
66
+ },
67
+ });
68
+ },
69
+ async getFullIndexingInProgress() {
70
+ const entries = await strapi.entityService.findMany('plugin::elasticsearch.task', {
71
+ filters: {
72
+ indexing_status: 'in-progress',
73
+ full_site_indexing: true,
74
+ },
75
+ });
76
+ return entries;
77
+ },
62
78
  });
@@ -60,9 +60,9 @@ exports.default = ({ strapi }) => {
60
60
  const privateIndexAlias = collection.indexAlias;
61
61
  const helper = getHelperService();
62
62
  let timestamp = Date.now();
63
+ let indexName = '';
63
64
  try {
64
65
  const esInterface = getElasticsearchService();
65
- let indexName;
66
66
  if (privateIndexAlias) {
67
67
  indexName = await helper.getIncrementedIndexName(privateIndexAlias);
68
68
  await esInterface.createIndex(indexName, collection.mappings);
@@ -71,14 +71,26 @@ exports.default = ({ strapi }) => {
71
71
  indexName = await helper.getCurrentIndexName();
72
72
  }
73
73
  let page = 0;
74
- let hasMoreData = true;
74
+ let prevPageData = [];
75
75
  let totalIndexed = 0;
76
- while (hasMoreData) {
76
+ const pageLimit = 10000;
77
+ while (page <= pageLimit) {
77
78
  const pageData = await collection.extractData(page);
79
+ strapi.log.debug(`Extracted ${pageData.length} items from ${collectionName} for page ${page}`);
78
80
  if (!Array.isArray(pageData) || pageData.length === 0) {
79
- hasMoreData = false;
80
81
  break;
81
82
  }
83
+ if (JSON.stringify(prevPageData) == JSON.stringify(pageData)) {
84
+ throw new Error(`Infinite loop detected at page ${page} while reindexing ${collectionName}. Stopping reindexing. Check this virtual-collection's extractData().
85
+ current page 1st item (id ${pageData[0]?.id}):
86
+ ${JSON.stringify(pageData[0])}
87
+ prev page 1st item (id ${prevPageData[0]?.id}):
88
+ ${JSON.stringify(prevPageData[0])}`);
89
+ }
90
+ if (page >= pageLimit) {
91
+ strapi.log.warn(`Page ${page} of ${collectionName} is greater than page-limit (${pageLimit}). stopping indexing this virtual-collection.`);
92
+ pageData.length = pageLimit;
93
+ }
82
94
  const operations = [];
83
95
  for (const itemData of pageData) {
84
96
  const itemId = collection.getIndexItemId(itemData.id, collectionName);
@@ -88,6 +100,7 @@ exports.default = ({ strapi }) => {
88
100
  await Promise.all(operations.map((op) => esInterface.indexDataToSpecificIndex(op, indexName)));
89
101
  }
90
102
  totalIndexed += pageData.length;
103
+ prevPageData = pageData;
91
104
  page++;
92
105
  }
93
106
  strapi.log.info(`Reindexed ${totalIndexed} items for virtual collection: ${collectionName}. took ${(0, humanize_duration_1.default)(Date.now() - timestamp)}. now updating alias.`);
@@ -102,7 +115,7 @@ exports.default = ({ strapi }) => {
102
115
  return totalIndexed;
103
116
  }
104
117
  catch (error) {
105
- strapi.log.error(`Error reindexing ${collectionName}: ${error?.message} after ${(0, humanize_duration_1.default)(Date.now() - timestamp)}`);
118
+ strapi.log.error(`Error reindexing ${collectionName} to index ${indexName}: ${error?.message} after ${(0, humanize_duration_1.default)(Date.now() - timestamp)}`);
106
119
  throw error;
107
120
  }
108
121
  },
@@ -36,20 +36,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const yup = __importStar(require("yup"));
37
37
  const isFunction = () => yup.mixed().test('is-function', `must be a function`, (value) => typeof value === 'function');
38
38
  const configSchema = yup.object({
39
- indexAlias: yup.string().nullable(),
39
+ indexAlias: yup.string().notRequired().nonNullable(),
40
40
  collectionName: yup.string().required(),
41
41
  extractData: isFunction().required(),
42
42
  extractByIds: isFunction().required(),
43
43
  getIndexItemId: isFunction(),
44
44
  triggers: yup
45
45
  .array()
46
+ .optional()
46
47
  .of(yup.object({
47
48
  collection: yup.string().required(),
48
- getIdsToReindex: isFunction(),
49
+ getIdsToReindex: isFunction().required(),
49
50
  alsoTriggerDelete: yup.boolean().default(false),
50
51
  }))
51
52
  .default([]),
52
- mappings: yup.object().default({}),
53
+ mappings: yup.object().optional(),
53
54
  });
54
55
  /**
55
56
  * Service to handle indexing of virtual collections
@@ -69,9 +70,10 @@ exports.default = ({ strapi }) => {
69
70
  if (!config) {
70
71
  const helper = strapi.plugin('elasticsearch').service('helper');
71
72
  const defaultConf = configSchema.getDefault();
72
- config = strapi.plugin('elasticsearch').config('virtualCollections') || [];
73
- config = config.map((collection) => {
74
- const collectionConfig = configSchema.validateSync(collection, { strict: true });
73
+ const virtualCollectionsFactories = strapi.plugin('elasticsearch').config('virtualCollections') || [];
74
+ config = virtualCollectionsFactories.map((factory) => {
75
+ let collectionConfig = factory(strapi);
76
+ collectionConfig = configSchema.validateSync(collectionConfig);
75
77
  collectionConfig.getIndexItemId = collectionConfig.getIndexItemId || ((id) => helper.getIndexItemId({ collectionName: collectionConfig.collectionName, itemId: id }));
76
78
  return { ...defaultConf, ...collectionConfig };
77
79
  });
@@ -1,4 +1,9 @@
1
1
  import { MappingTypeMapping } from '@elastic/elasticsearch/lib/api/types';
2
+ export type VirtualCollectionFactory = (strapi: any) => VirtualCollectionConfig;
3
+ export type ExtractByIdsFunction = (ids: number[]) => Promise<StrapiEntity[]>;
4
+ export type ExtractDataFunction = (page: number, pageSize?: number) => Promise<StrapiEntity[]>;
5
+ export type GetIndexItemIdFunction = (itemId: number, collectionName: string) => string;
6
+ export type GetIdsToIndexFunction = (event: any) => Promise<number[]>;
2
7
  export type VirtualCollectionConfig = {
3
8
  /**
4
9
  * Optional -
@@ -17,8 +22,8 @@ export type VirtualCollectionConfig = {
17
22
  * e.g 'api::restaurants.restaurants'.
18
23
  */
19
24
  collectionName: string;
20
- extractData: (page: number, pageSize?: number) => Promise<StrapiEntity[]>;
21
- extractByIds: (ids: number[]) => Promise<StrapiEntity[]>;
25
+ extractData: ExtractDataFunction;
26
+ extractByIds: ExtractByIdsFunction;
22
27
  /**
23
28
  * Optional -
24
29
  * A function that takes an item and returns the id of the item to be used in the index.
@@ -28,7 +33,7 @@ export type VirtualCollectionConfig = {
28
33
  * @param collectionName collection name. you probably want to use this to create a unique id for the item, especially if it is saved to the default index.
29
34
  * @returns the id of the item to be used in the index, _id. must be unique accross the index.
30
35
  */
31
- getIndexItemId?: (itemId: number, collectionName: string) => string;
36
+ getIndexItemId?: GetIndexItemIdFunction;
32
37
  triggers: Array<{
33
38
  /**
34
39
  * collection name to listen to for changes.
@@ -39,7 +44,7 @@ export type VirtualCollectionConfig = {
39
44
  * @param event - The event object containing the data to be indexed.
40
45
  * @returns ids of the items to be reindexed.
41
46
  */
42
- getIdsToReindex: (event: any) => Promise<number[]>;
47
+ getIdsToReindex: GetIdsToIndexFunction;
43
48
  /**
44
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.
45
50
  * defaults to false.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegan-friendly/strapi-plugin-elasticsearch",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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": {