@vegan-friendly/strapi-plugin-elasticsearch 0.0.11-alpha.7 → 0.1.0-alpha.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/.prettierrc
ADDED
package/.vscode/settings.json
CHANGED
@@ -1,3 +1,24 @@
|
|
1
1
|
{
|
2
2
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
3
|
+
"[javascript]": {
|
4
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
5
|
+
},
|
6
|
+
"[javascriptreact]": {
|
7
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
8
|
+
},
|
9
|
+
"[typescript]": {
|
10
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
11
|
+
},
|
12
|
+
"[typescriptreact]": {
|
13
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
14
|
+
},
|
15
|
+
"[jsonc]": {
|
16
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
17
|
+
},
|
18
|
+
"[json]": {
|
19
|
+
//redundant, but just in case
|
20
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
21
|
+
},
|
22
|
+
"editor.formatOnSaveMode": "modifications",
|
23
|
+
"editor.formatOnSave": true
|
3
24
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@vegan-friendly/strapi-plugin-elasticsearch",
|
3
|
-
"version": "0.0
|
3
|
+
"version": "0.1.0-alpha.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": {
|
package/server/services/index.js
CHANGED
@@ -7,6 +7,7 @@ const indexer = require('./perform-indexing');
|
|
7
7
|
const logIndexing = require('./log-indexing');
|
8
8
|
const helper = require('./helper');
|
9
9
|
const transformContent = require('./transform-content');
|
10
|
+
const virtualCollectionsRegistry = require('./virtual-collections-registry');
|
10
11
|
|
11
12
|
module.exports = {
|
12
13
|
configureIndexing,
|
@@ -15,5 +16,6 @@ module.exports = {
|
|
15
16
|
indexer,
|
16
17
|
logIndexing,
|
17
18
|
helper,
|
18
|
-
transformContent
|
19
|
+
transformContent,
|
20
|
+
virtualCollectionsRegistry,
|
19
21
|
};
|
@@ -0,0 +1,445 @@
|
|
1
|
+
/**
|
2
|
+
* Service to manage virtual collections for Elasticsearch indexing
|
3
|
+
*/
|
4
|
+
module.exports = ({ strapi }) => {
|
5
|
+
// Registry to hold all virtual collection definitions
|
6
|
+
const virtualCollections = new Map();
|
7
|
+
|
8
|
+
return {
|
9
|
+
/**
|
10
|
+
* Initialize the registry from the plugin configuration
|
11
|
+
*/
|
12
|
+
initialize() {
|
13
|
+
// Get the plugin configuration
|
14
|
+
const pluginConfig = strapi.config.get('plugin.elasticsearch') || {};
|
15
|
+
const virtualCollectionsConfig = pluginConfig.virtualCollections || [];
|
16
|
+
|
17
|
+
// Register each virtual collection from the config
|
18
|
+
virtualCollectionsConfig.forEach((config) => {
|
19
|
+
this.register(config);
|
20
|
+
});
|
21
|
+
|
22
|
+
strapi.log.info(
|
23
|
+
`Initialized ${virtualCollections.size} virtual collections for Elasticsearch indexing`
|
24
|
+
);
|
25
|
+
},
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Register a new virtual collection for indexing
|
29
|
+
* @param {Object} config - Configuration for the virtual collection
|
30
|
+
* @param {string} config.indexName - Name of the index in Elasticsearch
|
31
|
+
* @param {string} config.collectionName - Name identifier for the virtual collection
|
32
|
+
* @param {Function} config.extractData - Function that returns data to be indexed (with pagination)
|
33
|
+
* @param {Array<Object>} config.triggers - Array of collection triggers that should cause reindexing
|
34
|
+
* @param {Function} config.mapToIndex - Function that maps extracted data to Elasticsearch document
|
35
|
+
*/
|
36
|
+
register(config) {
|
37
|
+
if (!config.indexName || typeof config.indexName !== 'string') {
|
38
|
+
throw new Error('Virtual collection must have an indexName');
|
39
|
+
}
|
40
|
+
|
41
|
+
if (!config.collectionName || typeof config.collectionName !== 'string') {
|
42
|
+
throw new Error('Virtual collection must have a collectionName');
|
43
|
+
}
|
44
|
+
|
45
|
+
if (typeof config.extractData !== 'function') {
|
46
|
+
throw new Error('Virtual collection must have an extractData function');
|
47
|
+
}
|
48
|
+
|
49
|
+
if (!Array.isArray(config.triggers)) {
|
50
|
+
throw new Error('Virtual collection must have triggers defined');
|
51
|
+
}
|
52
|
+
|
53
|
+
if (typeof config.mapToIndex !== 'function') {
|
54
|
+
throw new Error('Virtual collection must have a mapToIndex function');
|
55
|
+
}
|
56
|
+
|
57
|
+
virtualCollections.set(config.collectionName, config);
|
58
|
+
strapi.log.info(
|
59
|
+
`Registered virtual collection for Elasticsearch: ${config.collectionName} -> ${config.indexName}`
|
60
|
+
);
|
61
|
+
|
62
|
+
return this;
|
63
|
+
},
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Get all registered virtual collections
|
67
|
+
*/
|
68
|
+
getAll() {
|
69
|
+
return Array.from(virtualCollections.values());
|
70
|
+
},
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Get a specific virtual collection by name
|
74
|
+
*/
|
75
|
+
get(collectionName) {
|
76
|
+
return virtualCollections.get(collectionName);
|
77
|
+
},
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Find virtual collections that should be triggered when a specific collection changes
|
81
|
+
*/
|
82
|
+
findTriggersByCollection(collectionUID) {
|
83
|
+
const results = [];
|
84
|
+
|
85
|
+
virtualCollections.forEach((config) => {
|
86
|
+
const hasTrigger = config.triggers.some((trigger) => trigger.collection === collectionUID);
|
87
|
+
|
88
|
+
if (hasTrigger) {
|
89
|
+
results.push(config);
|
90
|
+
}
|
91
|
+
});
|
92
|
+
|
93
|
+
return results;
|
94
|
+
},
|
95
|
+
};
|
96
|
+
};
|
97
|
+
|
98
|
+
// path: ./src/extensions/strapi-plugin-elasticsearch/services/virtual-collections-indexer.js
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Service to handle indexing of virtual collections
|
102
|
+
*/
|
103
|
+
module.exports = ({ strapi }) => {
|
104
|
+
const getElasticsearchService = () => strapi.plugin('elasticsearch').service('elasticsearch');
|
105
|
+
const getRegistryService = () =>
|
106
|
+
strapi.service('plugin::elasticsearch.virtual-collections-registry');
|
107
|
+
|
108
|
+
return {
|
109
|
+
/**
|
110
|
+
* Initialize indexes for all registered virtual collections
|
111
|
+
*/
|
112
|
+
async initializeIndexes() {
|
113
|
+
const registry = getRegistryService();
|
114
|
+
const collections = registry.getAll();
|
115
|
+
|
116
|
+
for (const collection of collections) {
|
117
|
+
await this.createIndexIfNotExists(collection.indexName);
|
118
|
+
}
|
119
|
+
},
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Create an Elasticsearch index if it doesn't exist
|
123
|
+
*/
|
124
|
+
async createIndexIfNotExists(indexName) {
|
125
|
+
const esService = getElasticsearchService();
|
126
|
+
const indexExists = await esService.indices.exists({ index: indexName });
|
127
|
+
|
128
|
+
if (!indexExists) {
|
129
|
+
await esService.indices.create({
|
130
|
+
index: indexName,
|
131
|
+
body: {
|
132
|
+
settings: {
|
133
|
+
analysis: {
|
134
|
+
analyzer: {
|
135
|
+
default: {
|
136
|
+
type: 'standard',
|
137
|
+
},
|
138
|
+
},
|
139
|
+
},
|
140
|
+
},
|
141
|
+
},
|
142
|
+
});
|
143
|
+
|
144
|
+
strapi.log.info(`Created Elasticsearch index: ${indexName}`);
|
145
|
+
}
|
146
|
+
},
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Index a single item from a virtual collection
|
150
|
+
*/
|
151
|
+
async indexItem(collectionName, itemId) {
|
152
|
+
const registry = getRegistryService();
|
153
|
+
const collection = registry.get(collectionName);
|
154
|
+
|
155
|
+
if (!collection) {
|
156
|
+
throw new Error(`Virtual collection not found: ${collectionName}`);
|
157
|
+
}
|
158
|
+
|
159
|
+
try {
|
160
|
+
// Extract data using the collection's extractData
|
161
|
+
// For a single item, we'll pass the ID as a filter parameter
|
162
|
+
const results = await collection.extractData(0, { id: itemId });
|
163
|
+
|
164
|
+
if (!results || !Array.isArray(results) || results.length === 0) {
|
165
|
+
strapi.log.warn(`No data extracted for ${collectionName} with ID ${itemId}`);
|
166
|
+
return null;
|
167
|
+
}
|
168
|
+
|
169
|
+
const data = results[0];
|
170
|
+
|
171
|
+
// Map the data to the index format
|
172
|
+
const indexData = collection.mapToIndex(data);
|
173
|
+
|
174
|
+
// Index the data
|
175
|
+
const esService = getElasticsearchService();
|
176
|
+
await esService.index({
|
177
|
+
index: collection.indexName,
|
178
|
+
id: itemId,
|
179
|
+
body: indexData,
|
180
|
+
refresh: true,
|
181
|
+
});
|
182
|
+
|
183
|
+
strapi.log.debug(`Indexed virtual item: ${collectionName}:${itemId}`);
|
184
|
+
return indexData;
|
185
|
+
} catch (error) {
|
186
|
+
strapi.log.error(`Error indexing ${collectionName}:${itemId}: ${error.message}`);
|
187
|
+
throw error;
|
188
|
+
}
|
189
|
+
},
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Reindex all items in a virtual collection
|
193
|
+
*/
|
194
|
+
async reindexAll(collectionName) {
|
195
|
+
const registry = getRegistryService();
|
196
|
+
const collection = registry.get(collectionName);
|
197
|
+
|
198
|
+
if (!collection) {
|
199
|
+
throw new Error(`Virtual collection not found: ${collectionName}`);
|
200
|
+
}
|
201
|
+
|
202
|
+
try {
|
203
|
+
// Create a new index with a timestamp
|
204
|
+
const timestamp = Date.now();
|
205
|
+
const tempIndexName = `${collection.indexName}_${timestamp}`;
|
206
|
+
|
207
|
+
// Create the temporary index
|
208
|
+
const esService = getElasticsearchService();
|
209
|
+
await esService.indices.create({
|
210
|
+
index: tempIndexName,
|
211
|
+
body: {
|
212
|
+
settings: {
|
213
|
+
analysis: {
|
214
|
+
analyzer: {
|
215
|
+
default: {
|
216
|
+
type: 'standard',
|
217
|
+
},
|
218
|
+
},
|
219
|
+
},
|
220
|
+
},
|
221
|
+
},
|
222
|
+
});
|
223
|
+
|
224
|
+
// Pagination variables
|
225
|
+
let page = 0;
|
226
|
+
let hasMoreData = true;
|
227
|
+
let totalIndexed = 0;
|
228
|
+
|
229
|
+
// Process data in batches
|
230
|
+
while (hasMoreData) {
|
231
|
+
// Extract data for current page
|
232
|
+
const pageData = await collection.extractData(page);
|
233
|
+
|
234
|
+
if (!Array.isArray(pageData) || pageData.length === 0) {
|
235
|
+
hasMoreData = false;
|
236
|
+
break;
|
237
|
+
}
|
238
|
+
|
239
|
+
// Bulk index items in this page
|
240
|
+
const operations = [];
|
241
|
+
|
242
|
+
for (const item of pageData) {
|
243
|
+
const indexData = collection.mapToIndex(item);
|
244
|
+
operations.push({
|
245
|
+
index: {
|
246
|
+
_index: tempIndexName,
|
247
|
+
_id: item.id,
|
248
|
+
},
|
249
|
+
});
|
250
|
+
operations.push(indexData);
|
251
|
+
}
|
252
|
+
|
253
|
+
if (operations.length > 0) {
|
254
|
+
await esService.bulk({ body: operations, refresh: true });
|
255
|
+
}
|
256
|
+
|
257
|
+
totalIndexed += pageData.length;
|
258
|
+
page++;
|
259
|
+
}
|
260
|
+
|
261
|
+
// Swap the indices
|
262
|
+
const oldIndex = collection.indexName;
|
263
|
+
const indexExists = await esService.indices.exists({ index: oldIndex });
|
264
|
+
|
265
|
+
if (indexExists) {
|
266
|
+
// Create or update alias
|
267
|
+
const aliasExists = await esService.indices.existsAlias({
|
268
|
+
name: collection.indexName,
|
269
|
+
});
|
270
|
+
|
271
|
+
if (aliasExists) {
|
272
|
+
// Update existing alias
|
273
|
+
await esService.indices.updateAliases({
|
274
|
+
body: {
|
275
|
+
actions: [
|
276
|
+
{ remove: { index: '_all', alias: collection.indexName } },
|
277
|
+
{ add: { index: tempIndexName, alias: collection.indexName } },
|
278
|
+
],
|
279
|
+
},
|
280
|
+
});
|
281
|
+
} else {
|
282
|
+
// Create new alias
|
283
|
+
await esService.indices.putAlias({
|
284
|
+
index: tempIndexName,
|
285
|
+
name: collection.indexName,
|
286
|
+
});
|
287
|
+
}
|
288
|
+
|
289
|
+
// Delete old indices with this prefix
|
290
|
+
const { body: indices } = await esService.indices.get({
|
291
|
+
index: `${collection.indexName}_*`,
|
292
|
+
});
|
293
|
+
|
294
|
+
for (const indexName in indices) {
|
295
|
+
if (indexName !== tempIndexName) {
|
296
|
+
await esService.indices.delete({ index: indexName });
|
297
|
+
}
|
298
|
+
}
|
299
|
+
} else {
|
300
|
+
// Just add the alias if the original index doesn't exist
|
301
|
+
await esService.indices.putAlias({
|
302
|
+
index: tempIndexName,
|
303
|
+
name: collection.indexName,
|
304
|
+
});
|
305
|
+
}
|
306
|
+
|
307
|
+
strapi.log.info(
|
308
|
+
`Reindexed ${totalIndexed} items for virtual collection: ${collectionName}`
|
309
|
+
);
|
310
|
+
return totalIndexed;
|
311
|
+
} catch (error) {
|
312
|
+
strapi.log.error(`Error reindexing ${collectionName}: ${error.message}`);
|
313
|
+
throw error;
|
314
|
+
}
|
315
|
+
},
|
316
|
+
|
317
|
+
/**
|
318
|
+
* Handle a trigger event from a collection
|
319
|
+
*/
|
320
|
+
async handleTriggerEvent(event) {
|
321
|
+
const { model, result } = event;
|
322
|
+
const registry = getRegistryService();
|
323
|
+
|
324
|
+
// Find virtual collections that should be triggered by this model
|
325
|
+
const affectedCollections = registry.findTriggersByCollection(model);
|
326
|
+
|
327
|
+
for (const collection of affectedCollections) {
|
328
|
+
// Find the specific trigger for this collection
|
329
|
+
const trigger = collection.triggers.find((t) => t.collection === model);
|
330
|
+
|
331
|
+
if (trigger && trigger.getIdsToReindex) {
|
332
|
+
// Get IDs that need to be reindexed
|
333
|
+
const idsToReindex = await trigger.getIdsToReindex(result);
|
334
|
+
|
335
|
+
// Reindex each item
|
336
|
+
for (const id of idsToReindex) {
|
337
|
+
await this.indexItem(collection.collectionName, id);
|
338
|
+
}
|
339
|
+
}
|
340
|
+
}
|
341
|
+
},
|
342
|
+
|
343
|
+
/**
|
344
|
+
* Delete an item from a virtual collection index
|
345
|
+
*/
|
346
|
+
async deleteItem(collectionName, itemId) {
|
347
|
+
const registry = getRegistryService();
|
348
|
+
const collection = registry.get(collectionName);
|
349
|
+
|
350
|
+
if (!collection) {
|
351
|
+
throw new Error(`Virtual collection not found: ${collectionName}`);
|
352
|
+
}
|
353
|
+
|
354
|
+
try {
|
355
|
+
const esService = getElasticsearchService();
|
356
|
+
await esService.delete({
|
357
|
+
index: collection.indexName,
|
358
|
+
id: itemId,
|
359
|
+
refresh: true,
|
360
|
+
});
|
361
|
+
|
362
|
+
strapi.log.debug(`Deleted indexed item: ${collectionName}:${itemId}`);
|
363
|
+
return true;
|
364
|
+
} catch (error) {
|
365
|
+
if (error.meta && error.meta.statusCode === 404) {
|
366
|
+
// Item not found - that's fine
|
367
|
+
return false;
|
368
|
+
}
|
369
|
+
strapi.log.error(`Error deleting ${collectionName}:${itemId}: ${error.message}`);
|
370
|
+
throw error;
|
371
|
+
}
|
372
|
+
},
|
373
|
+
};
|
374
|
+
};
|
375
|
+
|
376
|
+
// path: ./src/extensions/strapi-plugin-elasticsearch/strapi-server.js
|
377
|
+
|
378
|
+
module.exports = (plugin) => {
|
379
|
+
// Preserve original services, controllers, etc.
|
380
|
+
const originalServices = plugin.services || {};
|
381
|
+
|
382
|
+
// Add our new services
|
383
|
+
plugin.services = {
|
384
|
+
...originalServices,
|
385
|
+
'virtual-collections-registry': require('./services/virtual-collections-registry'),
|
386
|
+
'virtual-collections-indexer': require('./services/virtual-collections-indexer'),
|
387
|
+
};
|
388
|
+
|
389
|
+
// Extend the bootstrap function
|
390
|
+
const originalBootstrap = plugin.bootstrap;
|
391
|
+
plugin.bootstrap = async ({ strapi }) => {
|
392
|
+
// Call the original bootstrap if it exists
|
393
|
+
if (originalBootstrap) {
|
394
|
+
await originalBootstrap({ strapi });
|
395
|
+
}
|
396
|
+
|
397
|
+
// Initialize the registry from config
|
398
|
+
strapi.service('plugin::elasticsearch.virtual-collections-registry').initialize();
|
399
|
+
|
400
|
+
// Setup lifecycle hooks
|
401
|
+
const registry = strapi.service('plugin::elasticsearch.virtual-collections-registry');
|
402
|
+
const virtualCollections = registry.getAll();
|
403
|
+
|
404
|
+
// Create a set of all collections that need hooks
|
405
|
+
const collectionsToHook = new Set();
|
406
|
+
|
407
|
+
virtualCollections.forEach((collection) => {
|
408
|
+
collection.triggers.forEach((trigger) => {
|
409
|
+
collectionsToHook.add(trigger.collection);
|
410
|
+
});
|
411
|
+
});
|
412
|
+
|
413
|
+
// Setup hooks for each collection
|
414
|
+
collectionsToHook.forEach((collectionUID) => {
|
415
|
+
strapi.log.info(`Setting up Elasticsearch lifecycle hooks for collection: ${collectionUID}`);
|
416
|
+
|
417
|
+
strapi.db.lifecycles.subscribe({
|
418
|
+
models: [collectionUID],
|
419
|
+
|
420
|
+
afterCreate: async (event) => {
|
421
|
+
await strapi
|
422
|
+
.service('plugin::elasticsearch.virtual-collections-indexer')
|
423
|
+
.handleTriggerEvent(event);
|
424
|
+
},
|
425
|
+
|
426
|
+
afterUpdate: async (event) => {
|
427
|
+
await strapi
|
428
|
+
.service('plugin::elasticsearch.virtual-collections-indexer')
|
429
|
+
.handleTriggerEvent(event);
|
430
|
+
},
|
431
|
+
|
432
|
+
afterDelete: async (event) => {
|
433
|
+
await strapi
|
434
|
+
.service('plugin::elasticsearch.virtual-collections-indexer')
|
435
|
+
.handleTriggerEvent(event);
|
436
|
+
},
|
437
|
+
});
|
438
|
+
});
|
439
|
+
|
440
|
+
// Initialize indexes
|
441
|
+
await strapi.service('plugin::elasticsearch.virtual-collections-indexer').initializeIndexes();
|
442
|
+
};
|
443
|
+
|
444
|
+
return plugin;
|
445
|
+
};
|