add: medusa-plugin-meilisearch + add deleted event in productService + tweak emitted event data in productVariantService
This commit is contained in:
1 parent
4602cfc083
commit
c56f37415d
24 files changed
+14041
-3
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-transform-instanceof",
|
||||||
|
"@babel/plugin-transform-classes"
|
||||||
|
],
|
||||||
|
"presets": ["@babel/preset-env"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["@babel/plugin-transform-runtime"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"extends": ["prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"semi": "error",
|
||||||
|
"no-unused-expressions": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/dist
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
/uploads
|
||||||
|
/node_modules
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
/dist
|
||||||
|
|
||||||
|
/api
|
||||||
|
/services
|
||||||
|
/models
|
||||||
|
/subscribers
|
||||||
|
/loaders
|
||||||
|
/utils
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/lib
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
/*.js
|
||||||
|
!index.js
|
||||||
|
yarn.lock
|
||||||
|
src
|
||||||
|
.gitignore
|
||||||
|
.eslintrc
|
||||||
|
.babelrc
|
||||||
|
.prettierrc
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# medusa-plugin-meilisearch
|
||||||
|
|
||||||
|
Meilisearch Plugin for Medusa to search for products.
|
||||||
|
|
||||||
|
## Plugin Options
|
||||||
|
```
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
host: [your meilisearch host],
|
||||||
|
},
|
||||||
|
settings: [meilisearch settings passed to meilisearch's `updateSettings()` method on an index:
|
||||||
|
//example
|
||||||
|
{
|
||||||
|
searchableAttributes: ["title", "description", "sku"],
|
||||||
|
displayedAttributes: ["title", "description", "sku"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
//noop
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: "node",
|
||||||
|
}
|
||||||
+8205
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-plugin-meilisearch",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A starter for Medusa projects.",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/medusa-plugin-meilisearch"
|
||||||
|
},
|
||||||
|
"author": "Zakaria El Asri",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "babel src -d .",
|
||||||
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"medusa-interfaces": "1.x",
|
||||||
|
"typeorm": "0.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"medusa-core-utils": "^1.1.20",
|
||||||
|
"medusa-interfaces": "^1.1.21",
|
||||||
|
"meilisearch": "^0.20.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.7.5",
|
||||||
|
"@babel/core": "^7.7.5",
|
||||||
|
"@babel/node": "^7.7.4",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||||
|
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
|
"@babel/preset-env": "^7.7.5",
|
||||||
|
"@babel/register": "^7.7.4",
|
||||||
|
"@babel/runtime": "^7.9.6",
|
||||||
|
"client-sessions": "^0.8.0",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"jest": "^25.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import routes from "./routes/"
|
||||||
|
|
||||||
|
export default (container) => {
|
||||||
|
const app = Router()
|
||||||
|
|
||||||
|
routes(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { default as wrap } from "./await-middleware"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
wrap,
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const schema = Validator.object()
|
||||||
|
.keys({
|
||||||
|
q: Validator.string().required(),
|
||||||
|
})
|
||||||
|
.options({ allowUnknown: true })
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
console.log({ error })
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { q, ...options } = value
|
||||||
|
|
||||||
|
const meiliSearchService = req.scope.resolve("meilisearchService")
|
||||||
|
|
||||||
|
const results = await meiliSearchService.search(q, options)
|
||||||
|
|
||||||
|
res.status(200).send(results)
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
defaultProductFields,
|
||||||
|
defaultProductRelations,
|
||||||
|
flattenSkus,
|
||||||
|
} from "../utils"
|
||||||
|
|
||||||
|
export default async (container, options) => {
|
||||||
|
try {
|
||||||
|
const meilisearchService = container.resolve("meilisearchService")
|
||||||
|
const productService = container.resolve("productService")
|
||||||
|
|
||||||
|
const products = await productService.list(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
select: defaultProductFields,
|
||||||
|
relations: defaultProductRelations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const productsWithSkus = products.map((product) => flattenSkus(product))
|
||||||
|
|
||||||
|
await meilisearchService.updateSettings()
|
||||||
|
await meilisearchService.addDocuments(productsWithSkus)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { MeiliSearch } from "meilisearch"
|
||||||
|
|
||||||
|
class MeilisearchService extends BaseService {
|
||||||
|
constructor({ eventBusService }, options) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.eventBus_ = eventBusService
|
||||||
|
|
||||||
|
this.options_ = options
|
||||||
|
|
||||||
|
this.client_ = new MeiliSearch(options.config).index("products")
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllDocuments() {
|
||||||
|
return this.client_.deleteAllDocuments()
|
||||||
|
}
|
||||||
|
|
||||||
|
addDocuments(documents) {
|
||||||
|
return this.client_.addDocuments(documents)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDocument(document_id) {
|
||||||
|
return this.client_.deleteDocument(document_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query, options) {
|
||||||
|
return this.client_.search(query, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSettings() {
|
||||||
|
return this.client_.updateSettings(this.options_.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeilisearchService
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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) => {
|
||||||
|
console.log({ change: data })
|
||||||
|
const product = await this.retrieveProduct_(data.product_id)
|
||||||
|
console.log(product.variants)
|
||||||
|
await this.meilisearchService_.addDocuments([product])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeilisearchSubscriber
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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",
|
||||||
|
]
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -801,6 +801,7 @@ describe("ProductVariantService", () => {
|
|||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
|
product_id: IdMap.getId("product-test"),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -819,9 +820,20 @@ describe("ProductVariantService", () => {
|
|||||||
await productVariantService.delete(IdMap.getId("ironman"))
|
await productVariantService.delete(IdMap.getId("ironman"))
|
||||||
|
|
||||||
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
|
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
|
||||||
expect(productVariantRepository.softRemove).toBeCalledWith({
|
expect(productVariantRepository.softRemove).toBeCalledWith(
|
||||||
id: IdMap.getId("ironman"),
|
expect.objectContaining({
|
||||||
})
|
id: IdMap.getId("ironman"),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||||
|
"product-variant.deleted",
|
||||||
|
{
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
product_id: IdMap.getId("product-test"),
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully resolves if variant does not exist", async () => {
|
it("successfully resolves if variant does not exist", async () => {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ describe("ProductService", () => {
|
|||||||
const productService = new ProductService({
|
const productService = new ProductService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
productRepository,
|
productRepository,
|
||||||
|
eventBusService,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -318,6 +319,11 @@ describe("ProductService", () => {
|
|||||||
expect(productRepository.softRemove).toBeCalledWith({
|
expect(productRepository.softRemove).toBeCalledWith({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
expect(eventBusService.emit).toBeCalledTimes(1)
|
||||||
|
expect(eventBusService.emit).toBeCalledWith("product.deleted", {
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ProductVariantService extends BaseService {
|
|||||||
static Events = {
|
static Events = {
|
||||||
UPDATED: "product-variant.updated",
|
UPDATED: "product-variant.updated",
|
||||||
CREATED: "product-variant.created",
|
CREATED: "product-variant.created",
|
||||||
|
DELETED: "product-variant.deleted",
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param { productVariantModel: (ProductVariantModel) } */
|
/** @param { productVariantModel: (ProductVariantModel) } */
|
||||||
@@ -229,6 +230,7 @@ class ProductVariantService extends BaseService {
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.emit(ProductVariantService.Events.UPDATED, {
|
.emit(ProductVariantService.Events.UPDATED, {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
|
product_id: result.product_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -305,6 +307,7 @@ class ProductVariantService extends BaseService {
|
|||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.emit(ProductVariantService.Events.UPDATED, {
|
.emit(ProductVariantService.Events.UPDATED, {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
|
product_id: result.product_id,
|
||||||
fields: Object.keys(update),
|
fields: Object.keys(update),
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
@@ -587,6 +590,11 @@ class ProductVariantService extends BaseService {
|
|||||||
|
|
||||||
await variantRepo.softRemove(variant)
|
await variantRepo.softRemove(variant)
|
||||||
|
|
||||||
|
await this.eventBus_.emit(ProductVariantService.Events.DELETED, {
|
||||||
|
id: variant.id,
|
||||||
|
product_id: variant.product_id,
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class ProductService extends BaseService {
|
|||||||
static Events = {
|
static Events = {
|
||||||
UPDATED: "product.updated",
|
UPDATED: "product.updated",
|
||||||
CREATED: "product.created",
|
CREATED: "product.created",
|
||||||
|
DELETED: "product.deleted",
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@@ -473,6 +474,12 @@ class ProductService extends BaseService {
|
|||||||
|
|
||||||
await productRepo.softRemove(product)
|
await productRepo.softRemove(product)
|
||||||
|
|
||||||
|
await this.eventBus_
|
||||||
|
.withTransaction(manager)
|
||||||
|
.emit(ProductService.Events.DELETED, {
|
||||||
|
id: productId,
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user