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`