| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * @param {{ strapi: Strapi }} strapi | ||
| * @returns {Object} | ||
| */ | ||
| function createConfigController({ strapi }) { | ||
| return { | ||
| async get(ctx) { | ||
| const { excludedFields = [], prefix: indexPrefix = '', contentTypes } = strapi.config.get('plugin.search'); | ||
|
|
||
| ctx.send({ | ||
| excludedFields, | ||
| indexPrefix, | ||
| contentTypes, | ||
| }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { | ||
| createConfigController, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| 'use strict'; | ||
|
|
||
| const { createConfigController: config } = require('./config'); | ||
| const { createSearchController: search } = require('./search'); | ||
|
|
||
| module.exports = { | ||
| config, | ||
| search, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 'use strict'; | ||
|
|
||
| /** | ||
| * @param {{ strapi: Strapi }} strapi | ||
| * @returns {Object} | ||
| */ | ||
| function createSearchController({ strapi }) { | ||
| return { | ||
| async indexContentType(ctx) { | ||
| // validate that there is a param of contentType | ||
| if (!ctx.params.contentType) { | ||
| ctx.badRequest('contentType is required'); | ||
| } | ||
|
|
||
| // validate that the contentType is a valid content type | ||
| const contentType = strapi.contentTypes[ctx.params.contentType]; | ||
| if (!contentType) { | ||
| ctx.badRequest('contentType is not a valid content type'); | ||
| } | ||
|
|
||
| // send to search service | ||
| const { successCount, totalCount } = await strapi.plugin('search').service('search').indexContentType({ contentType: ctx.params.contentType }); | ||
|
|
||
| // send success response | ||
| ctx.send({ ok: true, successCount, totalCount }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { | ||
| createSearchController, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 'use strict'; | ||
|
|
||
| module.exports = [ | ||
| { | ||
| method: 'GET', | ||
| path: '/config', | ||
| handler: 'config.get', | ||
| }, | ||
| { | ||
| method: 'GET', | ||
| path: '/index/:contentType', | ||
| handler: 'search.indexContentType', | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| 'use strict'; | ||
|
|
||
| const { omit, pick } = require('lodash/fp'); | ||
| const { filter } = require('../utils/filters'); | ||
|
|
||
| function sanitizeEntry({ entry, fields, excludedFields }) { | ||
| if (fields.length > 0) { | ||
| return pick(fields, entry); | ||
| } | ||
|
|
||
| return omit(excludedFields, entry); | ||
| } | ||
|
|
||
| function getFirstLevelAttributeFromKey(key) { | ||
| return key.split('.')[0]; | ||
| } | ||
|
|
||
| /** | ||
| * Gets search service | ||
| * | ||
| * @returns {object} Search service | ||
| */ | ||
| module.exports = () => ({ | ||
| /** | ||
| * Index all entries for a given content type that aren't already indexed | ||
| */ | ||
| async indexContentType({ contentType }) { | ||
| const provider = strapi.plugin('search').provider; | ||
| const { excludedFields = [], prefix: indexPrefix = '', contentTypes } = strapi.config.get('plugin.search'); | ||
|
|
||
| const contentTypeConfigs = contentTypes.reduce((acc, contentType) => { | ||
| acc[contentType.name] = contentType; | ||
| return acc; | ||
| }, {}); | ||
|
|
||
| // get all entries for the content type | ||
| const entries = await strapi.entityService.findMany(contentType, { | ||
| fields: [ | ||
| ...(contentTypeConfigs[contentType].fields || []), | ||
| ...(contentTypeConfigs[contentType].filters ? Object.keys(contentTypeConfigs[contentType].filters).map(getFirstLevelAttributeFromKey) : []), | ||
| ], | ||
| }); | ||
|
|
||
| // provider.create each entry | ||
| const results = await Promise.allSettled( | ||
| entries.map(async (entry) => { | ||
| if (contentTypeConfigs[contentType].filters && !filter(entry, contentTypeConfigs[contentType].filters)) { | ||
| throw new Error(`Skipping index creation for content type: ${contentType} (filter returned false)`); | ||
| } | ||
|
|
||
| // get the full entry | ||
| /** | ||
| * TODO: this doesn't populate everything that is populated afterCreate/afterUpdate | ||
| * lifecycle method so we need to figure out how to get the full entry here - check | ||
| * out the afterCreate/afterUpdate lifecycle methods in the core repo | ||
| * */ | ||
| const fullEntry = await strapi.entityService.findOne(contentType, entry.id); | ||
|
|
||
| const sanitizedEntry = sanitizeEntry({ | ||
| entry: fullEntry, | ||
| fields: contentTypeConfigs[contentType].fields || [], | ||
| excludedFields, | ||
| }); | ||
|
|
||
| // get the index name | ||
| const indexName = indexPrefix + (contentTypeConfigs[contentType].index ? contentTypeConfigs[contentType].index : contentType); | ||
|
|
||
| // create the entry | ||
| /** | ||
| * TODO: to effectively return results here we need to convert this to an async | ||
| * function and get the result from algolia | ||
| */ | ||
| provider.create({ | ||
| indexName, | ||
| data: sanitizedEntry, | ||
| id: contentTypeConfigs[contentType].prefix + entry.id, | ||
| }); | ||
|
|
||
| return entry.id; | ||
| }), | ||
| ); | ||
|
|
||
| let successCount = 0; | ||
|
|
||
| // log successful indexing | ||
| results.forEach((result) => { | ||
| if (result.status === 'fulfilled') { | ||
| strapi.log.info(`Indexed ${contentType} entry ${result.value}`); | ||
| successCount++; | ||
| } | ||
| if (result.status === 'rejected') { | ||
| strapi.log.error(`Failed to index ${contentType} entry ${result.reason}`); | ||
| } | ||
| }); | ||
|
|
||
| // return summary | ||
| return { | ||
| successCount, | ||
| totalCount: entries.length, | ||
| }; | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| 'use strict'; | ||
|
|
||
| const _ = require('lodash'); | ||
|
|
||
| /** | ||
| * Evaluates filter object logic against entry | ||
| * | ||
| * @param {object} entry | ||
| * @param {object} filters | ||
| * @returns {boolean} | ||
| */ | ||
| const filter = (entry, filters) => { | ||
| if (!filters) { | ||
| return true; | ||
| } | ||
|
|
||
| // iterate through each filter and compare it to one, stop processing if any fail | ||
| return Object.entries(filters).every(([path, expectedValue]) => { | ||
| strapi.log.info(`Search plugin: path: ${path}`); | ||
| try { | ||
| const entryValue = _.get(entry, path); | ||
| strapi.log.info(`Search plugin: entry value: ${entryValue}`); | ||
| strapi.log.info(`Search plugin: expected value: ${expectedValue}`); | ||
|
|
||
| if (entryValue === undefined) { | ||
| return true; | ||
| } | ||
|
|
||
| if (typeof expectedValue !== 'object') { | ||
| return false; | ||
| } | ||
|
|
||
| const operator = Object.keys(expectedValue)[0]; | ||
|
|
||
| switch (operator) { | ||
| case '$eq': | ||
| strapi.log.info(`Search plugin: Comparing ${entryValue} $eq ${expectedValue[operator]}`); | ||
| return entryValue === expectedValue[operator]; | ||
| case '$ne': | ||
| strapi.log.info(`Search plugin: Comparing ${entryValue} $ne ${expectedValue[operator]}`); | ||
| return entryValue !== expectedValue[operator]; | ||
| default: | ||
| return false; | ||
| } | ||
| } catch (error) { | ||
| strapi.log.error(`Search plugin: Error running filter function: ${error.message}`); | ||
| return false; | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = { | ||
| filter, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| 'use strict'; | ||
|
|
||
| module.exports = require('./admin/src').default; |