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
This commit is contained in:
@@ -5,3 +5,4 @@ export { default as FulfillmentService } from "./fulfillment-service"
|
|||||||
export { default as FileService } from "./file-service"
|
export { default as FileService } from "./file-service"
|
||||||
export { default as NotificationService } from "./notification-service"
|
export { default as NotificationService } from "./notification-service"
|
||||||
export { default as OauthService } from "./oauth-service"
|
export { default as OauthService } from "./oauth-service"
|
||||||
|
export { default as SearchService } from "./search-service"
|
||||||
|
|||||||
93
packages/medusa-interfaces/src/search-service.js
Normal file
93
packages/medusa-interfaces/src/search-service.js
Normal file
@@ -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.<Object>} - 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
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"medusa-core-utils": "^1.1.20",
|
"medusa-core-utils": "^1.1.20",
|
||||||
"medusa-interfaces": "^1.1.21",
|
"medusa-interfaces": "^1.1.23",
|
||||||
"meilisearch": "^0.20.0"
|
"meilisearch": "^0.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Router } from "express"
|
|
||||||
import routes from "./routes/"
|
|
||||||
|
|
||||||
export default (container) => {
|
|
||||||
const app = Router()
|
|
||||||
|
|
||||||
routes(app)
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { default as wrap } from "./await-middleware"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
wrap,
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,14 @@
|
|||||||
import {
|
const INDEX_NS = "medusa-commerce"
|
||||||
defaultProductFields,
|
|
||||||
defaultProductRelations,
|
|
||||||
flattenSkus,
|
|
||||||
} from "../utils"
|
|
||||||
|
|
||||||
export default async (container, options) => {
|
export default async (container, options) => {
|
||||||
try {
|
try {
|
||||||
const meilisearchService = container.resolve("meilisearchService")
|
const searchService = container.resolve("searchService")
|
||||||
const productService = container.resolve("productService")
|
|
||||||
|
|
||||||
const products = await productService.list(
|
await Promise.all(
|
||||||
{},
|
Object.entries(options.settings).map(([key, value]) =>
|
||||||
{
|
searchService.updateSettings(`${INDEX_NS}_${key}`, value)
|
||||||
select: defaultProductFields,
|
)
|
||||||
relations: defaultProductRelations,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const productsWithSkus = products.map((product) => flattenSkus(product))
|
|
||||||
|
|
||||||
await meilisearchService.updateSettings()
|
|
||||||
await meilisearchService.addDocuments(productsWithSkus)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,46 @@
|
|||||||
import { BaseService } from "medusa-interfaces"
|
import { SearchService } from "medusa-interfaces"
|
||||||
import { MeiliSearch } from "meilisearch"
|
import { MeiliSearch } from "meilisearch"
|
||||||
|
|
||||||
class MeilisearchService extends BaseService {
|
class MeiliSearchService extends SearchService {
|
||||||
constructor({ eventBusService }, options) {
|
constructor(container, options) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.eventBus_ = eventBusService
|
|
||||||
|
|
||||||
this.options_ = options
|
this.options_ = options
|
||||||
|
|
||||||
this.client_ = new MeiliSearch(options.config).index("products")
|
this.client_ = new MeiliSearch(options.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAllDocuments() {
|
createIndex(indexName, options) {
|
||||||
return this.client_.deleteAllDocuments()
|
return this.client_.createIndex(indexName, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
addDocuments(documents) {
|
getIndex(indexName) {
|
||||||
return this.client_.addDocuments(documents)
|
return this.client_.index(indexName)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDocument(document_id) {
|
addDocuments(indexName, documents) {
|
||||||
return this.client_.deleteDocument(document_id)
|
return this.client_.index(indexName).addDocuments(documents)
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query, options) {
|
replaceDocuments(indexName, documents) {
|
||||||
return this.client_.search(query, options)
|
return this.client_.index(indexName).addDocuments(documents)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings() {
|
deleteDocument(indexName, document_id) {
|
||||||
return this.client_.updateSettings(this.options_.settings)
|
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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -3777,20 +3777,20 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
medusa-core-utils@^1.1.20:
|
medusa-core-utils@^1.1.22:
|
||||||
version "1.1.20"
|
version "1.1.22"
|
||||||
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.20.tgz#676c0dc863a206b80cc53299a984c532d07df65f"
|
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.22.tgz#84ce0af0a7c672191d758ea462056e30a39d08b1"
|
||||||
integrity sha512-gf+/L5eeqHea3xgjwD7YZEzfUGlxbjfvaeiiGWi3Wfu0dLa+G1B4S0TsX+upR+oVeWPmk66VMqWC80h3e4csqw==
|
integrity sha512-kMuRkWOuNG4Bw6epg/AYu95UJuE+rjHTeTWRLbEPrYGjWREV82tLWVDI21/QcccmaHmMU98Rkw2z9JwyFZIiyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
joi "^17.3.0"
|
joi "^17.3.0"
|
||||||
joi-objectid "^3.0.1"
|
joi-objectid "^3.0.1"
|
||||||
|
|
||||||
medusa-interfaces@^1.1.21:
|
medusa-interfaces@^1.1.23:
|
||||||
version "1.1.21"
|
version "1.1.23"
|
||||||
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.21.tgz#ca86808e939b7ecc21a6d316008a4e41f163619f"
|
resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.23.tgz#b552a8c1d0eaddeff30472ab238652b9e1a56e73"
|
||||||
integrity sha512-mlHHoMIOFBc+Exs+uVIQsfeEP2C1Pi6IZHcpbm7O00tYBdQdqRjJre9+Z/I/Z37wt5IwA28/TIoVkYG71iQYxw==
|
integrity sha512-dHCOnsyYQvjrtRd3p0ZqQZ4M/zmo4M/BAgVfRrYSyGrMdQ86TK9Z1DQDCHEzM1216AxEfXz2JYUD7ilTfG2iHQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
medusa-core-utils "^1.1.20"
|
medusa-core-utils "^1.1.22"
|
||||||
|
|
||||||
meilisearch@^0.20.0:
|
meilisearch@^0.20.0:
|
||||||
version "0.20.0"
|
version "0.20.0"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import returnReasonRoutes from "./return-reasons"
|
|||||||
import swapRoutes from "./swaps"
|
import swapRoutes from "./swaps"
|
||||||
import variantRoutes from "./variants"
|
import variantRoutes from "./variants"
|
||||||
import giftCardRoutes from "./gift-cards"
|
import giftCardRoutes from "./gift-cards"
|
||||||
|
import searchRoutes from "./search"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export default (app, container, config) => {
|
|||||||
returnRoutes(route)
|
returnRoutes(route)
|
||||||
giftCardRoutes(route)
|
giftCardRoutes(route)
|
||||||
returnReasonRoutes(route)
|
returnReasonRoutes(route)
|
||||||
|
searchRoutes(route)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/medusa/src/api/routes/store/search/index.js
Normal file
12
packages/medusa/src/api/routes/store/search/index.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Validator, MedusaError } from "medusa-core-utils"
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
import { INDEX_NS } from "../../../../utils/index-ns"
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const schema = Validator.object()
|
const schema = Validator.object()
|
||||||
.keys({
|
.keys({
|
||||||
q: Validator.string().required(),
|
q: Validator.string().required(),
|
||||||
|
indexName: Validator.string().required(),
|
||||||
})
|
})
|
||||||
.options({ allowUnknown: true })
|
.options({ allowUnknown: true })
|
||||||
|
|
||||||
@@ -13,11 +15,15 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)
|
res.status(200).send(results)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -20,6 +20,9 @@ const silentResolution = (container, name, logger) => {
|
|||||||
case "fulfillmentProviders":
|
case "fulfillmentProviders":
|
||||||
identifier = "fulfillment"
|
identifier = "fulfillment"
|
||||||
break
|
break
|
||||||
|
case "searchService":
|
||||||
|
identifier = "search"
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
identifier = name
|
identifier = name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import passportLoader from "./passport"
|
|||||||
import pluginsLoader, { registerPluginModels } from "./plugins"
|
import pluginsLoader, { registerPluginModels } from "./plugins"
|
||||||
import defaultsLoader from "./defaults"
|
import defaultsLoader from "./defaults"
|
||||||
import Logger from "./logger"
|
import Logger from "./logger"
|
||||||
|
import searchIndexLoader from "./search-index"
|
||||||
|
|
||||||
export default async ({ directory: rootDirectory, expressApp }) => {
|
export default async ({ directory: rootDirectory, expressApp }) => {
|
||||||
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||||
@@ -101,12 +102,6 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
|||||||
const servAct = Logger.success(servicesActivity, "Services initialized") || {}
|
const servAct = Logger.success(servicesActivity, "Services initialized") || {}
|
||||||
track("SERVICES_INIT_COMPLETED", { duration: servAct.duration })
|
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")
|
const expActivity = Logger.activity("Initializing express")
|
||||||
track("EXPRESS_INIT_STARTED")
|
track("EXPRESS_INIT_STARTED")
|
||||||
await expressLoader({
|
await expressLoader({
|
||||||
@@ -138,6 +133,12 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
|||||||
const pAct = Logger.success(pluginsActivity, "Plugins intialized") || {}
|
const pAct = Logger.success(pluginsActivity, "Plugins intialized") || {}
|
||||||
track("PLUGINS_INIT_COMPLETED", { duration: pAct.duration })
|
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")
|
const apiActivity = Logger.activity("Initializing API")
|
||||||
track("API_INIT_STARTED")
|
track("API_INIT_STARTED")
|
||||||
await apiLoader({
|
await apiLoader({
|
||||||
@@ -155,6 +156,12 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
|||||||
const dAct = Logger.success(defaultsActivity, "Defaults initialized") || {}
|
const dAct = Logger.success(defaultsActivity, "Defaults initialized") || {}
|
||||||
track("DEFAULTS_INIT_COMPLETED", { duration: dAct.duration })
|
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 }
|
return { container, dbConnection, app: expressApp }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
NotificationService,
|
NotificationService,
|
||||||
FileService,
|
FileService,
|
||||||
OauthService,
|
OauthService,
|
||||||
|
SearchService,
|
||||||
} from "medusa-interfaces"
|
} from "medusa-interfaces"
|
||||||
import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
|
import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
@@ -249,7 +250,7 @@ async function registerServices(pluginDetails, container) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Add the service directly to the container in order to make simple
|
// 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({
|
container.register({
|
||||||
[name]: asFunction(
|
[name]: asFunction(
|
||||||
cradle => new loaded(cradle, pluginDetails.options)
|
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
|
// 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({
|
container.register({
|
||||||
[name]: asFunction(
|
[name]: asFunction(
|
||||||
cradle => new loaded(cradle, pluginDetails.options)
|
cradle => new loaded(cradle, pluginDetails.options)
|
||||||
@@ -272,13 +273,22 @@ async function registerServices(pluginDetails, container) {
|
|||||||
})
|
})
|
||||||
} else if (loaded.prototype instanceof FileService) {
|
} else if (loaded.prototype instanceof FileService) {
|
||||||
// Add the service directly to the container in order to make simple
|
// 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({
|
container.register({
|
||||||
[name]: asFunction(
|
[name]: asFunction(
|
||||||
cradle => new loaded(cradle, pluginDetails.options)
|
cradle => new loaded(cradle, pluginDetails.options)
|
||||||
),
|
),
|
||||||
[`fileService`]: aliasTo(name),
|
[`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 {
|
} else {
|
||||||
container.register({
|
container.register({
|
||||||
[name]: asFunction(
|
[name]: asFunction(
|
||||||
|
|||||||
13
packages/medusa/src/loaders/search-index.js
Normal file
13
packages/medusa/src/loaders/search-index.js
Normal file
@@ -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()
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@ import _ from "lodash"
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import { Brackets } from "typeorm"
|
import { Brackets } from "typeorm"
|
||||||
|
import { flattenField } from "../utils/flatten-field"
|
||||||
|
import { INDEX_NS } from "../utils/index-ns"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides layer to manipulate products.
|
* Provides layer to manipulate products.
|
||||||
* @implements BaseService
|
* @implements BaseService
|
||||||
*/
|
*/
|
||||||
class ProductService extends BaseService {
|
class ProductService extends BaseService {
|
||||||
|
static IndexName = `${INDEX_NS}_products`
|
||||||
static Events = {
|
static Events = {
|
||||||
UPDATED: "product.updated",
|
UPDATED: "product.updated",
|
||||||
CREATED: "product.created",
|
CREATED: "product.created",
|
||||||
@@ -25,6 +28,7 @@ class ProductService extends BaseService {
|
|||||||
productTypeRepository,
|
productTypeRepository,
|
||||||
productTagRepository,
|
productTagRepository,
|
||||||
imageRepository,
|
imageRepository,
|
||||||
|
searchService,
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -57,6 +61,9 @@ class ProductService extends BaseService {
|
|||||||
|
|
||||||
/** @private @const {ImageRepository} */
|
/** @private @const {ImageRepository} */
|
||||||
this.imageRepository_ = imageRepository
|
this.imageRepository_ = imageRepository
|
||||||
|
|
||||||
|
/** @private @const {SearchService} */
|
||||||
|
this.searchService_ = searchService
|
||||||
}
|
}
|
||||||
|
|
||||||
withTransaction(transactionManager) {
|
withTransaction(transactionManager) {
|
||||||
@@ -741,6 +748,43 @@ class ProductService extends BaseService {
|
|||||||
// const final = await this.runDecorators_(decorated)
|
// const final = await this.runDecorators_(decorated)
|
||||||
return product
|
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
|
export default ProductService
|
||||||
|
|||||||
66
packages/medusa/src/services/search.js
Normal file
66
packages/medusa/src/services/search.js
Normal file
@@ -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
|
||||||
89
packages/medusa/src/subscribers/search.js
Normal file
89
packages/medusa/src/subscribers/search.js
Normal file
@@ -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
|
||||||
3
packages/medusa/src/utils/flatten-field.js
Normal file
3
packages/medusa/src/utils/flatten-field.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const flattenField = (list, field) => {
|
||||||
|
return list.map(el => el[field])
|
||||||
|
}
|
||||||
1
packages/medusa/src/utils/index-ns.js
Normal file
1
packages/medusa/src/utils/index-ns.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const INDEX_NS = `medusa-commerce`
|
||||||
Reference in New Issue
Block a user