add: medusa-plugin-meilisearch + add deleted event in productService + tweak emitted event data in productVariantService

This commit is contained in:
zakariaelas
2021-09-09 14:02:05 +01:00
parent 4602cfc083
commit c56f37415d
24 changed files with 14041 additions and 3 deletions

View File

@@ -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"]
}
}
}

View File

@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -0,0 +1,15 @@
/dist
.env
.DS_Store
/uploads
/node_modules
yarn-error.log
/dist
/api
/services
/models
/subscribers
/loaders
/utils

View File

@@ -0,0 +1,13 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@@ -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"],
}
],
}
```

View File

@@ -0,0 +1 @@
//noop

View File

@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: "node",
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -0,0 +1,10 @@
import { Router } from "express"
import routes from "./routes/"
export default (container) => {
const app = Router()
routes(app)
return app
}

View File

@@ -0,0 +1 @@
export default (fn) => (...args) => fn(...args).catch(args[2])

View File

@@ -0,0 +1,5 @@
import { default as wrap } from "./await-middleware"
export default {
wrap,
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -801,6 +801,7 @@ describe("ProductVariantService", () => {
}
return Promise.resolve({
id: IdMap.getId("ironman"),
product_id: IdMap.getId("product-test"),
})
},
})
@@ -819,9 +820,20 @@ describe("ProductVariantService", () => {
await productVariantService.delete(IdMap.getId("ironman"))
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
expect(productVariantRepository.softRemove).toBeCalledWith({
id: IdMap.getId("ironman"),
})
expect(productVariantRepository.softRemove).toBeCalledWith(
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 () => {

View File

@@ -306,6 +306,7 @@ describe("ProductService", () => {
const productService = new ProductService({
manager: MockManager,
productRepository,
eventBusService,
})
beforeEach(() => {
@@ -318,6 +319,11 @@ describe("ProductService", () => {
expect(productRepository.softRemove).toBeCalledWith({
id: IdMap.getId("ironman"),
})
expect(eventBusService.emit).toBeCalledTimes(1)
expect(eventBusService.emit).toBeCalledWith("product.deleted", {
id: IdMap.getId("ironman"),
})
})
})

View File

@@ -11,6 +11,7 @@ class ProductVariantService extends BaseService {
static Events = {
UPDATED: "product-variant.updated",
CREATED: "product-variant.created",
DELETED: "product-variant.deleted",
}
/** @param { productVariantModel: (ProductVariantModel) } */
@@ -229,6 +230,7 @@ class ProductVariantService extends BaseService {
.withTransaction(manager)
.emit(ProductVariantService.Events.UPDATED, {
id: result.id,
product_id: result.product_id,
})
return result
@@ -305,6 +307,7 @@ class ProductVariantService extends BaseService {
.withTransaction(manager)
.emit(ProductVariantService.Events.UPDATED, {
id: result.id,
product_id: result.product_id,
fields: Object.keys(update),
})
return result
@@ -587,6 +590,11 @@ class ProductVariantService extends BaseService {
await variantRepo.softRemove(variant)
await this.eventBus_.emit(ProductVariantService.Events.DELETED, {
id: variant.id,
product_id: variant.product_id,
})
return Promise.resolve()
})
}

View File

@@ -11,6 +11,7 @@ class ProductService extends BaseService {
static Events = {
UPDATED: "product.updated",
CREATED: "product.created",
DELETED: "product.deleted",
}
constructor({
@@ -473,6 +474,12 @@ class ProductService extends BaseService {
await productRepo.softRemove(product)
await this.eventBus_
.withTransaction(manager)
.emit(ProductService.Events.DELETED, {
id: productId,
})
return Promise.resolve()
})
}