From 0fbde7c51f1e85fc8981b67b44416eb11248a31b Mon Sep 17 00:00:00 2001 From: zakariaelas Date: Wed, 15 Sep 2021 22:42:17 +0100 Subject: [PATCH] add: abstract search functionality to core + adjust meilisearch-plugin add SearchService interface to medusa-interfaces add DefaultSearchService skeleton implementation to core add search-index.js loader to core for indexing db documents add SearchSubscriber to core add loadToSearchEngine method in ProductService switch order of loaders in core to load subscriptions AFTER plugins adjust service and loader for medusa-plugin-meilisearch --- packages/medusa-interfaces/src/index.js | 1 + .../medusa-interfaces/src/search-service.js | 93 +++++++++++++++++++ .../medusa-plugin-meilisearch/package.json | 2 +- .../src/api/index.js | 10 -- .../src/api/middlewares/await-middleware.js | 1 - .../src/api/middlewares/index.js | 5 - .../src/api/routes/index.js | 17 ---- .../src/loaders/index.js | 23 ++--- .../src/services/meilisearch.js | 44 +++++---- .../src/subscribers/meilisearch.js | 69 -------------- .../src/utils/index.js | 29 ------ packages/medusa-plugin-meilisearch/yarn.lock | 18 ++-- packages/medusa/src/api/routes/store/index.js | 2 + .../src/api/routes/store/search/index.js | 12 +++ .../src/api/routes/store/search/search.js} | 12 ++- packages/medusa/src/loaders/defaults.js | 3 + packages/medusa/src/loaders/index.js | 19 ++-- packages/medusa/src/loaders/plugins.js | 16 +++- packages/medusa/src/loaders/search-index.js | 13 +++ packages/medusa/src/services/product.js | 44 +++++++++ packages/medusa/src/services/search.js | 66 +++++++++++++ packages/medusa/src/subscribers/search.js | 89 ++++++++++++++++++ packages/medusa/src/utils/flatten-field.js | 3 + packages/medusa/src/utils/index-ns.js | 1 + 24 files changed, 405 insertions(+), 187 deletions(-) create mode 100644 packages/medusa-interfaces/src/search-service.js delete mode 100644 packages/medusa-plugin-meilisearch/src/api/index.js delete mode 100644 packages/medusa-plugin-meilisearch/src/api/middlewares/await-middleware.js delete mode 100644 packages/medusa-plugin-meilisearch/src/api/middlewares/index.js delete mode 100644 packages/medusa-plugin-meilisearch/src/api/routes/index.js delete mode 100644 packages/medusa-plugin-meilisearch/src/subscribers/meilisearch.js delete mode 100644 packages/medusa-plugin-meilisearch/src/utils/index.js create mode 100644 packages/medusa/src/api/routes/store/search/index.js rename packages/{medusa-plugin-meilisearch/src/api/routes/meilisearch.js => medusa/src/api/routes/store/search/search.js} (59%) create mode 100644 packages/medusa/src/loaders/search-index.js create mode 100644 packages/medusa/src/services/search.js create mode 100644 packages/medusa/src/subscribers/search.js create mode 100644 packages/medusa/src/utils/flatten-field.js create mode 100644 packages/medusa/src/utils/index-ns.js diff --git a/packages/medusa-interfaces/src/index.js b/packages/medusa-interfaces/src/index.js index 94d2d3a774..b88b5dca74 100644 --- a/packages/medusa-interfaces/src/index.js +++ b/packages/medusa-interfaces/src/index.js @@ -5,3 +5,4 @@ export { default as FulfillmentService } from "./fulfillment-service" export { default as FileService } from "./file-service" export { default as NotificationService } from "./notification-service" export { default as OauthService } from "./oauth-service" +export { default as SearchService } from "./search-service" diff --git a/packages/medusa-interfaces/src/search-service.js b/packages/medusa-interfaces/src/search-service.js new file mode 100644 index 0000000000..7b42848afc --- /dev/null +++ b/packages/medusa-interfaces/src/search-service.js @@ -0,0 +1,93 @@ +import { BaseService } from "medusa-interfaces" + +/** + * The interface that all search services must implement. + * @interface + */ +class SearchService extends BaseService { + constructor() { + super() + } + + /** + * Used to create an index + * @param indexName {string} - the index name. + * @param [options] {string} - the index name. + * @return {Promise<{object}>} - returns response from search engine provider + */ + createIndex(indexName, options) { + throw Error("createIndex must be overridden by a child class") + } + + /** + * Used to get an index + * @param indexName {string} - the index name. + * @return {Promise<{object}>} - returns response from search engine provider + */ + getIndex(indexName) { + throw Error("getIndex must be overridden by a child class") + } + + /** + * Used to index documents by the search engine provider. + * @param indexName {string} - the index name. + * @param documents {Array.} - documents array to be indexed + * @return {Promise<{object}>} - returns response from search engine provider + */ + addDocuments(indexName, documents) { + throw Error("addDocuments must be overridden by a child class") + } + + /** + * Used to replace documents + * @param indexName {string} - the index name. + * @param documents {Object} - array of document objects that will replace existing documents + * @return {Promise<{object}>} - returns response from search engine provider + */ + replaceDocuments(indexName, documents) { + throw Error("updateDocument must be overridden by a child class") + } + + /** + * Used to delete document + * @param indexName {string} - the index name. + * @param document_id {string} - the id of the document. + * @return {Promise<{object}>} - returns response from search engine provider + */ + deleteDocument(indexName, document_id) { + throw Error("deleteDocument must be overridden by a child class") + } + + /** + * Used to delete all documents + * @param indexName {string} - the index name. + * @return {Promise<{object}>} - returns response from search engine provider + */ + deleteAllDocuments(indexName) { + throw Error("deleteAllDocuments must be overridden by a child class") + } + + /** + * Used to search for a document in an index + * @param indexName {string} - the index name. + * @param query {string} - the search query. + * @param options {object} - any options passed to the request object other than the query and indexName + * e.g. pagination options, filtering options, etc. + * @return {Promise<{object}>} - returns response from search engine provider + */ + search(indexName, query, options) { + throw Error("search must be overridden by a child class") + } + + /** + * Used to update the settings of an index + * @param indexName {string} - the index name. + * @param settings {object} - settings object + * @return {Promise<{object}>} - returns response from search engine provider + */ + updateSettings(indexName, settings) { + throw Error("updateSettings must be overridden by a child class") + } +} + +export default SearchService diff --git a/packages/medusa-plugin-meilisearch/package.json b/packages/medusa-plugin-meilisearch/package.json index 711c88f1ff..3cd4948d5f 100644 --- a/packages/medusa-plugin-meilisearch/package.json +++ b/packages/medusa-plugin-meilisearch/package.json @@ -24,7 +24,7 @@ "body-parser": "^1.19.0", "lodash": "^4.17.21", "medusa-core-utils": "^1.1.20", - "medusa-interfaces": "^1.1.21", + "medusa-interfaces": "^1.1.23", "meilisearch": "^0.20.0" }, "devDependencies": { diff --git a/packages/medusa-plugin-meilisearch/src/api/index.js b/packages/medusa-plugin-meilisearch/src/api/index.js deleted file mode 100644 index 468335e29b..0000000000 --- a/packages/medusa-plugin-meilisearch/src/api/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from "express" -import routes from "./routes/" - -export default (container) => { - const app = Router() - - routes(app) - - return app -} diff --git a/packages/medusa-plugin-meilisearch/src/api/middlewares/await-middleware.js b/packages/medusa-plugin-meilisearch/src/api/middlewares/await-middleware.js deleted file mode 100644 index 1c3692b377..0000000000 --- a/packages/medusa-plugin-meilisearch/src/api/middlewares/await-middleware.js +++ /dev/null @@ -1 +0,0 @@ -export default (fn) => (...args) => fn(...args).catch(args[2]) diff --git a/packages/medusa-plugin-meilisearch/src/api/middlewares/index.js b/packages/medusa-plugin-meilisearch/src/api/middlewares/index.js deleted file mode 100644 index c784e319a9..0000000000 --- a/packages/medusa-plugin-meilisearch/src/api/middlewares/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { default as wrap } from "./await-middleware" - -export default { - wrap, -} diff --git a/packages/medusa-plugin-meilisearch/src/api/routes/index.js b/packages/medusa-plugin-meilisearch/src/api/routes/index.js deleted file mode 100644 index 3f6b31adb7..0000000000 --- a/packages/medusa-plugin-meilisearch/src/api/routes/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from "express" -import bodyParser from "body-parser" -import middlewares from "../middlewares" - -const route = Router() - -export default (app) => { - app.use("/meilisearch", route) - - route.post( - "/search", - bodyParser.json(), - middlewares.wrap(require("./meilisearch").default) - ) - - return app -} diff --git a/packages/medusa-plugin-meilisearch/src/loaders/index.js b/packages/medusa-plugin-meilisearch/src/loaders/index.js index 1dd7e53b2d..9093057245 100644 --- a/packages/medusa-plugin-meilisearch/src/loaders/index.js +++ b/packages/medusa-plugin-meilisearch/src/loaders/index.js @@ -1,25 +1,14 @@ -import { - defaultProductFields, - defaultProductRelations, - flattenSkus, -} from "../utils" +const INDEX_NS = "medusa-commerce" export default async (container, options) => { try { - const meilisearchService = container.resolve("meilisearchService") - const productService = container.resolve("productService") + const searchService = container.resolve("searchService") - const products = await productService.list( - {}, - { - select: defaultProductFields, - relations: defaultProductRelations, - } + await Promise.all( + Object.entries(options.settings).map(([key, value]) => + searchService.updateSettings(`${INDEX_NS}_${key}`, value) + ) ) - const productsWithSkus = products.map((product) => flattenSkus(product)) - - await meilisearchService.updateSettings() - await meilisearchService.addDocuments(productsWithSkus) } catch (err) { console.log(err) } diff --git a/packages/medusa-plugin-meilisearch/src/services/meilisearch.js b/packages/medusa-plugin-meilisearch/src/services/meilisearch.js index c263adc4f9..8f903f13d7 100644 --- a/packages/medusa-plugin-meilisearch/src/services/meilisearch.js +++ b/packages/medusa-plugin-meilisearch/src/services/meilisearch.js @@ -1,36 +1,46 @@ -import { BaseService } from "medusa-interfaces" +import { SearchService } from "medusa-interfaces" import { MeiliSearch } from "meilisearch" -class MeilisearchService extends BaseService { - constructor({ eventBusService }, options) { +class MeiliSearchService extends SearchService { + constructor(container, options) { super() - this.eventBus_ = eventBusService - this.options_ = options - this.client_ = new MeiliSearch(options.config).index("products") + this.client_ = new MeiliSearch(options.config) } - deleteAllDocuments() { - return this.client_.deleteAllDocuments() + createIndex(indexName, options) { + return this.client_.createIndex(indexName, options) } - addDocuments(documents) { - return this.client_.addDocuments(documents) + getIndex(indexName) { + return this.client_.index(indexName) } - deleteDocument(document_id) { - return this.client_.deleteDocument(document_id) + addDocuments(indexName, documents) { + return this.client_.index(indexName).addDocuments(documents) } - search(query, options) { - return this.client_.search(query, options) + replaceDocuments(indexName, documents) { + return this.client_.index(indexName).addDocuments(documents) } - updateSettings() { - return this.client_.updateSettings(this.options_.settings) + deleteDocument(indexName, document_id) { + return this.client_.index(indexName).deleteDocument(document_id) + } + + deleteAllDocuments(indexName) { + return this.client_.index(indexName).deleteAllDocuments() + } + + search(indexName, query, options) { + return this.client_.index(indexName).search(query, options) + } + + updateSettings(indexName, settings) { + return this.client_.index(indexName).updateSettings(settings) } } -export default MeilisearchService +export default MeiliSearchService diff --git a/packages/medusa-plugin-meilisearch/src/subscribers/meilisearch.js b/packages/medusa-plugin-meilisearch/src/subscribers/meilisearch.js deleted file mode 100644 index e161d6c3b4..0000000000 --- a/packages/medusa-plugin-meilisearch/src/subscribers/meilisearch.js +++ /dev/null @@ -1,69 +0,0 @@ -import { - defaultProductRelations, - defaultProductFields, - flattenSkus, -} from "../utils" - -class MeilisearchSubscriber { - constructor( - { eventBusService, meilisearchService, productService }, - options - ) { - this.eventBus_ = eventBusService - - this.meilisearchService_ = meilisearchService - - this.productService_ = productService - - this.eventBus_.subscribe("product.created", this.handleProductCreation) - - this.eventBus_.subscribe("product.updated", this.handleProductUpdate) - - this.eventBus_.subscribe("product.deleted", this.handleProductDeletion) - - this.eventBus_.subscribe( - "product-variant.created", - this.handleProductVariantChange - ) - - this.eventBus_.subscribe( - "product-variant.updated", - this.handleProductVariantChange - ) - - this.eventBus_.subscribe( - "product-variant.deleted", - this.handleProductVariantChange - ) - } - - handleProductCreation = async (data) => { - const product = await this.retrieveProduct_(data.id) - await this.meilisearchService_.addDocuments([product]) - } - - retrieveProduct_ = async (product_id) => { - const product = await this.productService_.retrieve(product_id, { - relations: defaultProductRelations, - select: defaultProductFields, - }) - const flattenedProduct = flattenSkus(product) - return flattenedProduct - } - - handleProductUpdate = async (data) => { - const product = await this.retrieveProduct_(data.id) - await this.meilisearchService_.addDocuments([product]) - } - - handleProductDeletion = async (data) => { - await this.meilisearchService_.deleteDocument(data.id) - } - - handleProductVariantChange = async (data) => { - const product = await this.retrieveProduct_(data.product_id) - await this.meilisearchService_.addDocuments([product]) - } -} - -export default MeilisearchSubscriber diff --git a/packages/medusa-plugin-meilisearch/src/utils/index.js b/packages/medusa-plugin-meilisearch/src/utils/index.js deleted file mode 100644 index 784e0bd5e4..0000000000 --- a/packages/medusa-plugin-meilisearch/src/utils/index.js +++ /dev/null @@ -1,29 +0,0 @@ -export const flattenSkus = (product) => { - const skus = product.variants.map((v) => v.sku).filter(Boolean) - product.sku = skus - return product -} - -export const defaultProductFields = [ - "id", - "title", - "subtitle", - "description", - "handle", - "is_giftcard", - "discountable", - "thumbnail", - "profile_id", - "collection_id", - "type_id", - "origin_country", - "created_at", - "updated_at", -] - -export const defaultProductRelations = [ - "variants", - "tags", - "type", - "collection", -] diff --git a/packages/medusa-plugin-meilisearch/yarn.lock b/packages/medusa-plugin-meilisearch/yarn.lock index b4204ffde8..dec1edc80c 100644 --- a/packages/medusa-plugin-meilisearch/yarn.lock +++ b/packages/medusa-plugin-meilisearch/yarn.lock @@ -3777,20 +3777,20 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^1.1.20: - version "1.1.20" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.20.tgz#676c0dc863a206b80cc53299a984c532d07df65f" - integrity sha512-gf+/L5eeqHea3xgjwD7YZEzfUGlxbjfvaeiiGWi3Wfu0dLa+G1B4S0TsX+upR+oVeWPmk66VMqWC80h3e4csqw== +medusa-core-utils@^1.1.22: + version "1.1.22" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.22.tgz#84ce0af0a7c672191d758ea462056e30a39d08b1" + integrity sha512-kMuRkWOuNG4Bw6epg/AYu95UJuE+rjHTeTWRLbEPrYGjWREV82tLWVDI21/QcccmaHmMU98Rkw2z9JwyFZIiyw== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@^1.1.21: - version "1.1.21" - resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.21.tgz#ca86808e939b7ecc21a6d316008a4e41f163619f" - integrity sha512-mlHHoMIOFBc+Exs+uVIQsfeEP2C1Pi6IZHcpbm7O00tYBdQdqRjJre9+Z/I/Z37wt5IwA28/TIoVkYG71iQYxw== +medusa-interfaces@^1.1.23: + version "1.1.23" + resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.23.tgz#b552a8c1d0eaddeff30472ab238652b9e1a56e73" + integrity sha512-dHCOnsyYQvjrtRd3p0ZqQZ4M/zmo4M/BAgVfRrYSyGrMdQ86TK9Z1DQDCHEzM1216AxEfXz2JYUD7ilTfG2iHQ== dependencies: - medusa-core-utils "^1.1.20" + medusa-core-utils "^1.1.22" meilisearch@^0.20.0: version "0.20.0" diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index 413ea3237b..adf675ab4f 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -15,6 +15,7 @@ import returnReasonRoutes from "./return-reasons" import swapRoutes from "./swaps" import variantRoutes from "./variants" import giftCardRoutes from "./gift-cards" +import searchRoutes from "./search" const route = Router() @@ -43,6 +44,7 @@ export default (app, container, config) => { returnRoutes(route) giftCardRoutes(route) returnReasonRoutes(route) + searchRoutes(route) return app } diff --git a/packages/medusa/src/api/routes/store/search/index.js b/packages/medusa/src/api/routes/store/search/index.js new file mode 100644 index 0000000000..3f71489d9f --- /dev/null +++ b/packages/medusa/src/api/routes/store/search/index.js @@ -0,0 +1,12 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/search", route) + + route.post("/", middlewares.wrap(require("./search").default)) + + return app +} diff --git a/packages/medusa-plugin-meilisearch/src/api/routes/meilisearch.js b/packages/medusa/src/api/routes/store/search/search.js similarity index 59% rename from packages/medusa-plugin-meilisearch/src/api/routes/meilisearch.js rename to packages/medusa/src/api/routes/store/search/search.js index 02aed2f439..567fdac1fa 100644 --- a/packages/medusa-plugin-meilisearch/src/api/routes/meilisearch.js +++ b/packages/medusa/src/api/routes/store/search/search.js @@ -1,9 +1,11 @@ import { Validator, MedusaError } from "medusa-core-utils" +import { INDEX_NS } from "../../../../utils/index-ns" export default async (req, res) => { const schema = Validator.object() .keys({ q: Validator.string().required(), + indexName: Validator.string().required(), }) .options({ allowUnknown: true }) @@ -13,11 +15,15 @@ export default async (req, res) => { } try { - const { q, ...options } = value + const { q, indexName, ...options } = value - const meiliSearchService = req.scope.resolve("meilisearchService") + const searchService = req.scope.resolve("searchService") - const results = await meiliSearchService.search(q, options) + const results = await searchService.search( + `${INDEX_NS}_${indexName}`, + q, + options + ) res.status(200).send(results) } catch (error) { diff --git a/packages/medusa/src/loaders/defaults.js b/packages/medusa/src/loaders/defaults.js index 2cabe9c3ee..553bc8222e 100644 --- a/packages/medusa/src/loaders/defaults.js +++ b/packages/medusa/src/loaders/defaults.js @@ -20,6 +20,9 @@ const silentResolution = (container, name, logger) => { case "fulfillmentProviders": identifier = "fulfillment" break + case "searchService": + identifier = "search" + break default: identifier = name } diff --git a/packages/medusa/src/loaders/index.js b/packages/medusa/src/loaders/index.js index fe40a35b72..e2ff64ae1d 100644 --- a/packages/medusa/src/loaders/index.js +++ b/packages/medusa/src/loaders/index.js @@ -16,6 +16,7 @@ import passportLoader from "./passport" import pluginsLoader, { registerPluginModels } from "./plugins" import defaultsLoader from "./defaults" import Logger from "./logger" +import searchIndexLoader from "./search-index" export default async ({ directory: rootDirectory, expressApp }) => { const { configModule } = getConfigFile(rootDirectory, `medusa-config`) @@ -101,12 +102,6 @@ export default async ({ directory: rootDirectory, expressApp }) => { const servAct = Logger.success(servicesActivity, "Services initialized") || {} track("SERVICES_INIT_COMPLETED", { duration: servAct.duration }) - const subActivity = Logger.activity("Initializing subscribers") - track("SUBSCRIBERS_INIT_STARTED") - subscribersLoader({ container, activityId: subActivity }) - const subAct = Logger.success(subActivity, "Subscribers initialized") || {} - track("SUBSCRIBERS_INIT_COMPLETED", { duration: subAct.duration }) - const expActivity = Logger.activity("Initializing express") track("EXPRESS_INIT_STARTED") await expressLoader({ @@ -138,6 +133,12 @@ export default async ({ directory: rootDirectory, expressApp }) => { const pAct = Logger.success(pluginsActivity, "Plugins intialized") || {} track("PLUGINS_INIT_COMPLETED", { duration: pAct.duration }) + const subActivity = Logger.activity("Initializing subscribers") + track("SUBSCRIBERS_INIT_STARTED") + subscribersLoader({ container, activityId: subActivity }) + const subAct = Logger.success(subActivity, "Subscribers initialized") || {} + track("SUBSCRIBERS_INIT_COMPLETED", { duration: subAct.duration }) + const apiActivity = Logger.activity("Initializing API") track("API_INIT_STARTED") await apiLoader({ @@ -155,6 +156,12 @@ export default async ({ directory: rootDirectory, expressApp }) => { const dAct = Logger.success(defaultsActivity, "Defaults initialized") || {} track("DEFAULTS_INIT_COMPLETED", { duration: dAct.duration }) + const searchActivity = Logger.activity("Initializing search engine indexing") + track("SEARCH_ENGINE_INDEXING_STARTED") + searchIndexLoader({ container, activityId: searchActivity }) + const searchAct = Logger.success(searchActivity, "Indexing completed") || {} + track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) + return { container, dbConnection, app: expressApp } } diff --git a/packages/medusa/src/loaders/plugins.js b/packages/medusa/src/loaders/plugins.js index e4b88acf27..e24bb63bef 100644 --- a/packages/medusa/src/loaders/plugins.js +++ b/packages/medusa/src/loaders/plugins.js @@ -8,6 +8,7 @@ import { NotificationService, FileService, OauthService, + SearchService, } from "medusa-interfaces" import { getConfigFile, createRequireFromPath } from "medusa-core-utils" import _ from "lodash" @@ -249,7 +250,7 @@ async function registerServices(pluginDetails, container) { ) // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use + // resolution if we already know which fulfillment provider we need to use container.register({ [name]: asFunction( cradle => new loaded(cradle, pluginDetails.options) @@ -263,7 +264,7 @@ async function registerServices(pluginDetails, container) { ) // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use + // resolution if we already know which notification provider we need to use container.register({ [name]: asFunction( cradle => new loaded(cradle, pluginDetails.options) @@ -272,13 +273,22 @@ async function registerServices(pluginDetails, container) { }) } else if (loaded.prototype instanceof FileService) { // Add the service directly to the container in order to make simple - // resolution if we already know which payment provider we need to use + // resolution if we already know which file storage provider we need to use container.register({ [name]: asFunction( cradle => new loaded(cradle, pluginDetails.options) ), [`fileService`]: aliasTo(name), }) + } else if (loaded.prototype instanceof SearchService) { + // Add the service directly to the container in order to make simple + // resolution if we already know which search provider we need to use + container.register({ + [name]: asFunction( + cradle => new loaded(cradle, pluginDetails.options) + ), + [`searchService`]: aliasTo(name), + }) } else { container.register({ [name]: asFunction( diff --git a/packages/medusa/src/loaders/search-index.js b/packages/medusa/src/loaders/search-index.js new file mode 100644 index 0000000000..9dfa81ee24 --- /dev/null +++ b/packages/medusa/src/loaders/search-index.js @@ -0,0 +1,13 @@ +export default async ({ container }) => { + const searchService = container.resolve("searchService") + const logger = container.resolve("logger") + if (searchService.isDefault) { + logger.warn( + "No search engine provider was found: make sure to include a search plugin to enable searching" + ) + return + } + + const productService = container.resolve("productService") + await productService.loadIntoSearchEngine() +} diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index a2ce2637af..5521594be6 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -2,12 +2,15 @@ import _ from "lodash" import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { Brackets } from "typeorm" +import { flattenField } from "../utils/flatten-field" +import { INDEX_NS } from "../utils/index-ns" /** * Provides layer to manipulate products. * @implements BaseService */ class ProductService extends BaseService { + static IndexName = `${INDEX_NS}_products` static Events = { UPDATED: "product.updated", CREATED: "product.created", @@ -25,6 +28,7 @@ class ProductService extends BaseService { productTypeRepository, productTagRepository, imageRepository, + searchService, }) { super() @@ -57,6 +61,9 @@ class ProductService extends BaseService { /** @private @const {ImageRepository} */ this.imageRepository_ = imageRepository + + /** @private @const {SearchService} */ + this.searchService_ = searchService } withTransaction(transactionManager) { @@ -741,6 +748,43 @@ class ProductService extends BaseService { // const final = await this.runDecorators_(decorated) return product } + + async loadIntoSearchEngine() { + if (this.searchService_.isDefault) return + + const products = await this.list( + {}, + { + select: [ + "id", + "title", + "subtitle", + "description", + "handle", + "is_giftcard", + "discountable", + "thumbnail", + "profile_id", + "collection_id", + "type_id", + "origin_country", + "created_at", + "updated_at", + ], + relations: ["variants", "tags", "type", "collection"], + } + ) + const flattenSkus = product => { + product.sku = flattenField(product.variants, "sku").filter(Boolean) + return product + } + const productsWithSkus = products.map(product => flattenSkus(product)) + + await this.searchService_.addDocuments( + ProductService.IndexName, + productsWithSkus + ) + } } export default ProductService diff --git a/packages/medusa/src/services/search.js b/packages/medusa/src/services/search.js new file mode 100644 index 0000000000..d6615becfd --- /dev/null +++ b/packages/medusa/src/services/search.js @@ -0,0 +1,66 @@ +import { SearchService } from "medusa-interfaces" + +/** + * Default class that implements SearchService but provides stuv implementation for all methods + * @implements SearchService + */ +class DefaultSearchService extends SearchService { + constructor(container) { + super() + + this.isDefault = true + + this.logger_ = container.logger + } + + createIndex(indexName, options) { + this.logger_.warn( + "This is an empty method: createIndex must be overridden by a child class" + ) + } + + getIndex(indexName) { + this.logger_.warn( + "This is an empty method: getIndex must be overridden by a child class" + ) + } + + addDocuments(indexName, documents) { + this.logger_.warn( + "This is an empty method: addDocuments must be overridden by a child class" + ) + } + + replaceDocuments(indexName, documents) { + this.logger_.warn( + "This is an empty method: replaceDocuments must be overridden by a child class" + ) + } + + deleteDocument(indexName, document_id) { + this.logger_.warn( + "This is an empty method: deleteDocument must be overridden by a child class" + ) + } + + deleteAllDocuments(indexName) { + this.logger_.warn( + "This is an empty method: deleteAllDocuments must be overridden by a child class" + ) + } + + search(indexName, query, options) { + this.logger_.warn( + "This is an empty method: search must be overridden a the child class" + ) + return [] + } + + updateSettings(indexName, settings) { + this.logger_.warn( + "This is an empty method: updateSettings must be overridden by a child class" + ) + } +} + +export default DefaultSearchService diff --git a/packages/medusa/src/subscribers/search.js b/packages/medusa/src/subscribers/search.js new file mode 100644 index 0000000000..e54089f003 --- /dev/null +++ b/packages/medusa/src/subscribers/search.js @@ -0,0 +1,89 @@ +import ProductService from "../services/product" +import ProductVariantService from "../services/product-variant" +import { flattenField } from "../utils/flatten-field" + +class SearchSubscriber { + constructor({ eventBusService, searchService, productService }, options) { + this.eventBus_ = eventBusService + + this.searchService_ = searchService + + this.productService_ = productService + + this.eventBus_.subscribe( + ProductService.Events.CREATED, + this.handleProductCreation + ) + + this.eventBus_.subscribe( + ProductService.Events.UPDATED, + this.handleProductUpdate + ) + + this.eventBus_.subscribe( + ProductService.Events.DELETED, + this.handleProductDeletion + ) + + this.eventBus_.subscribe( + ProductVariantService.Events.CREATED, + this.handleProductVariantChange + ) + + this.eventBus_.subscribe( + ProductVariantService.Events.UPDATED, + this.handleProductVariantChange + ) + + this.eventBus_.subscribe( + ProductVariantService.Events.DELETED, + this.handleProductVariantChange + ) + } + + handleProductCreation = async data => { + const product = await this.retrieveProduct_(data.id) + await this.searchService_.addDocuments(ProductService.IndexName, [product]) + } + + retrieveProduct_ = async product_id => { + const product = await this.productService_.retrieve(product_id, { + select: [ + "id", + "title", + "subtitle", + "description", + "handle", + "is_giftcard", + "discountable", + "thumbnail", + "profile_id", + "collection_id", + "type_id", + "origin_country", + "created_at", + "updated_at", + ], + relations: ["variants", "tags", "type", "collection"], + }) + const skus = flattenField(product.variants, "sku").filter(Boolean) + product.sku = skus + return product + } + + handleProductUpdate = async data => { + const product = await this.retrieveProduct_(data.id) + await this.searchService_.addDocuments(ProductService.IndexName, [product]) + } + + handleProductDeletion = async data => { + await this.searchService_.deleteDocument(ProductService.IndexName, data.id) + } + + handleProductVariantChange = async data => { + const product = await this.retrieveProduct_(data.product_id) + await this.searchService_.addDocuments(ProductService.IndexName, [product]) + } +} + +export default SearchSubscriber diff --git a/packages/medusa/src/utils/flatten-field.js b/packages/medusa/src/utils/flatten-field.js new file mode 100644 index 0000000000..2f6998ed09 --- /dev/null +++ b/packages/medusa/src/utils/flatten-field.js @@ -0,0 +1,3 @@ +export const flattenField = (list, field) => { + return list.map(el => el[field]) +} diff --git a/packages/medusa/src/utils/index-ns.js b/packages/medusa/src/utils/index-ns.js new file mode 100644 index 0000000000..caf7027dcc --- /dev/null +++ b/packages/medusa/src/utils/index-ns.js @@ -0,0 +1 @@ +export const INDEX_NS = `medusa-commerce`