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:
1 parent
425c8a5e5d
commit
0fbde7c51f
24 files changed
+405
-187
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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 })
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const { q, indexName, ...options } = value
|
||||
|
||||
const searchService = req.scope.resolve("searchService")
|
||||
|
||||
const results = await searchService.search(
|
||||
`${INDEX_NS}_${indexName}`,
|
||||
q,
|
||||
options
|
||||
)
|
||||
|
||||
res.status(200).send(results)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ const silentResolution = (container, name, logger) => {
|
||||
case "fulfillmentProviders":
|
||||
identifier = "fulfillment"
|
||||
break
|
||||
case "searchService":
|
||||
identifier = "search"
|
||||
break
|
||||
default:
|
||||
identifier = name
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 { 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
export const flattenField = (list, field) => {
|
||||
return list.map(el => el[field])
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const INDEX_NS = `medusa-commerce`
|
||||
Reference in new issue
Block a user