feat(medusa-plugin-meilisearch): Update + improve Meilisearch plugin (#3377)

* feat(medusa-plugin-meilisearch): Upgrade meilisearch deps + migrate plugin to TS

* fix version

* Remove transaction base service from search service

* Create .changeset/strange-mails-pump.md

* Backward compatibility

* Address PR feedback

* Fix folder structure

* Update readme

* Move types

* fix deps

* Change version in changeset

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
This commit is contained in:
Oliver Windall Juhl
2023-03-16 16:15:29 +01:00
committed by GitHub
parent 4213326fe8
commit 7e17e0ddc2
21 changed files with 377 additions and 217 deletions

View File

@@ -0,0 +1,65 @@
---
"medusa-plugin-meilisearch": major
"@medusajs/medusa": patch
---
feat(medusa-plugin-meilisearch): Update + improve Meilisearch plugin
**What**
- Bumps `meilisearch` dep to latest major
- Migrates plugin to TypeScript
- Changes the way indexes are configured in `medusa-config.js`:
**Before**
```
{
resolve: `medusa-plugin-meilisearch`,
options: {
config: { host: "...", apiKey: "..." },
settings: {
products: {
searchableAttributes: ["title"],
displayedAttributes: ["title"],
},
},
},
},
```
**After**
```
{
resolve: `medusa-plugin-meilisearch`,
options: {
config: { host: "...", apiKey: "..." },
settings: {
products: {
**indexSettings**: {
searchableAttributes: ["title"],
displayedAttributes: ["title"],
},
},
},
},
},
```
This is done to allow for additional configuration of indexes, that are not necessarily passed on query-time.
We introduce two new settings:
```
settings: {
products: {
indexSettings: {
searchableAttributes: ["title"],
displayedAttributes: ["title"],,
},
primaryKey: "id"
transformer: (document) => ({ id: "yo" })
},
},
```
Meilisearch changed their primary key inference in the major release. Now we must be explicit when multiple properties end with `id`. Read more in their [docs](https://docs.meilisearch.com/learn/core_concepts/primary_key.html#primary-field).
The transformer allows developers to specify how their documents are stored in Meilisearch. It is configurable for each index.

View File

@@ -1,13 +0,0 @@
{
"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

@@ -1,8 +0,0 @@
/src/subscribers
/api
/services
/models
/subscribers
/loaders
/utils

View File

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

View File

@@ -1,9 +0,0 @@
.DS_store
src
dist
yarn.lock
.babelrc
jest.config.js
.turbo
.yarn

View File

@@ -15,8 +15,12 @@ Learn more about how you can use this plugin in the [documentaion](https://docs.
"[indexName]": "[meilisearch settings passed to meilisearch's `updateSettings()` method]"
// example
products: {
searchableAttributes: ["title", "description", "variant_sku"],
displayedAttributes: ["title", "description", "variant_sku", "thumbnail", "handle"],
indexSettings: {
searchableAttributes: ["title", "description", "variant_sku"],
displayedAttributes: ["title", "description", "variant_sku", "thumbnail", "handle"],
},
primaryKey: "some_id"
transformer: (product: Product) => ({ id: product.id })
}
}
}

View File

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

View File

@@ -1,3 +1,13 @@
module.exports = {
testEnvironment: "node",
globals: {
"ts-jest": {
tsconfig: "tsconfig.spec.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "ts-jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}

View File

@@ -1,44 +1,39 @@
{
"name": "medusa-plugin-meilisearch",
"version": "1.0.4",
"description": "A starter for Medusa projects.",
"main": "index.js",
"description": "Meilisearch search plugin for Medusa",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-plugin-meilisearch"
},
"author": "Zakaria El Asri",
"files": [
"dist"
],
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build",
"test": "jest --passWithNoTests src",
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
"build": "tsc",
"watch": "tsc --watch"
},
"peerDependencies": {
"medusa-interfaces": "1.3.6"
"@medusajs/medusa": "^1.7.12",
"medusa-interfaces": "^1.3.6"
},
"dependencies": {
"body-parser": "^1.19.0",
"lodash": "^4.17.21",
"medusa-core-utils": "^1.1.39",
"meilisearch": "0.27.0"
"meilisearch": "^0.31.1"
},
"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",
"@medusajs/medusa": "^1.7.12",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.6"
"medusa-interfaces": "^1.3.6",
"typescript": "^4.9.5"
},
"gitHead": "cd1f5afa5aa8c0b15ea957008ee19f1d695cbd2e",
"keywords": [

View File

@@ -1,14 +0,0 @@
export default async (container, options) => {
try {
const meilisearchService = container.resolve("meilisearchService")
await Promise.all(
Object.entries(options.settings).map(([key, value]) =>
meilisearchService.updateSettings(key, value)
)
)
} catch (err) {
// ignore
console.log(err)
}
}

View File

@@ -0,0 +1,26 @@
import { Logger, MedusaContainer } from "@medusajs/modules-sdk"
import MeiliSearchService from "../services/meilisearch"
import { MeilisearchPluginOptions } from "../types"
export default async (
container: MedusaContainer,
options: MeilisearchPluginOptions
) => {
const logger: Logger = container.resolve("logger")
try {
const meilisearchService: MeiliSearchService =
container.resolve("meilisearchService")
const { settings } = options
await Promise.all(
Object.entries(settings ?? []).map(([indexName, value]) =>
meilisearchService.updateSettings(indexName, value)
)
)
} catch (err) {
// ignore
logger.warn(err)
}
}

View File

@@ -1,73 +0,0 @@
import { indexTypes } from "medusa-core-utils"
import { SearchService } from "medusa-interfaces"
import { MeiliSearch } from "meilisearch"
import { transformProduct } from "../utils/transform-product"
class MeiliSearchService extends SearchService {
constructor(container, options) {
super()
this.options_ = options
this.client_ = new MeiliSearch(options.config)
}
async createIndex(indexName, options) {
return await this.client_.createIndex(indexName, options)
}
getIndex(indexName) {
return this.client_.index(indexName)
}
async addDocuments(indexName, documents, type) {
const transformedDocuments = this.getTransformedDocuments(type, documents)
return await this.client_
.index(indexName)
.addDocuments(transformedDocuments)
}
async replaceDocuments(indexName, documents, type) {
const transformedDocuments = this.getTransformedDocuments(type, documents)
return await this.client_
.index(indexName)
.addDocuments(transformedDocuments)
}
async deleteDocument(indexName, document_id) {
return await this.client_.index(indexName).deleteDocument(document_id)
}
async deleteAllDocuments(indexName) {
return await this.client_.index(indexName).deleteAllDocuments()
}
async search(indexName, query, options) {
const { paginationOptions, filter, additionalOptions } = options
return await this.client_
.index(indexName)
.search(query, { filter, ...paginationOptions, ...additionalOptions })
}
async updateSettings(indexName, settings) {
return await this.client_.index(indexName).updateSettings(settings)
}
getTransformedDocuments(type, documents) {
switch (type) {
case indexTypes.products:
return this.transformProducts(documents)
default:
return documents
}
}
transformProducts(products) {
if (!products) {
return []
}
return products.map(transformProduct)
}
}
export default MeiliSearchService

View File

@@ -0,0 +1,124 @@
import { AbstractSearchService } from "@medusajs/medusa"
import { indexTypes } from "medusa-core-utils"
import { MeiliSearch, Settings } from "meilisearch"
import { IndexSettings, meilisearchErrorCodes, MeilisearchPluginOptions } from "../types"
import { transformProduct } from "../utils/transform-product"
class MeiliSearchService extends AbstractSearchService {
isDefault = false
protected readonly config_: MeilisearchPluginOptions
protected readonly client_: MeiliSearch
constructor(_, options: MeilisearchPluginOptions) {
super(_, options)
this.config_ = options
if (process.env.NODE_ENV !== "development") {
if (!options.config?.apiKey) {
throw Error(
"Meilisearch API key is missing in plugin config. See https://docs.medusajs.com/add-plugins/meilisearch"
)
}
}
if (!options.config?.host) {
throw Error(
"Meilisearch host is missing in plugin config. See https://docs.medusajs.com/add-plugins/meilisearch"
)
}
this.client_ = new MeiliSearch(options.config)
}
async createIndex(
indexName: string,
options: Record<string, unknown> = { primaryKey: "id" }
) {
return await this.client_.createIndex(indexName, options)
}
getIndex(indexName: string) {
return this.client_.index(indexName)
}
async addDocuments(indexName: string, documents: any, type: string) {
const transformedDocuments = this.getTransformedDocuments(type, documents)
return await this.client_
.index(indexName)
.addDocuments(transformedDocuments)
}
async replaceDocuments(indexName: string, documents: any, type: string) {
const transformedDocuments = this.getTransformedDocuments(type, documents)
return await this.client_
.index(indexName)
.addDocuments(transformedDocuments)
}
async deleteDocument(indexName: string, documentId: string) {
return await this.client_.index(indexName).deleteDocument(documentId)
}
async deleteAllDocuments(indexName: string) {
return await this.client_.index(indexName).deleteAllDocuments()
}
async search(indexName: string, query: string, options: Record<string, any>) {
const { paginationOptions, filter, additionalOptions } = options
return await this.client_
.index(indexName)
.search(query, { filter, ...paginationOptions, ...additionalOptions })
}
async updateSettings(
indexName: string,
settings: IndexSettings | Record<string, unknown>
) {
// backward compatibility
if (!("indexSettings" in settings)) {
settings = { indexSettings: settings }
}
await this.upsertIndex(indexName, settings as IndexSettings)
return await this.client_
.index(indexName)
.updateSettings(settings.indexSettings as Settings)
}
async upsertIndex(indexName: string, settings: IndexSettings) {
try {
await this.client_.getIndex(indexName)
} catch (error) {
if (error.code === meilisearchErrorCodes.INDEX_NOT_FOUND) {
await this.createIndex(indexName, {
primaryKey: settings?.primaryKey ?? "id",
})
}
}
}
getTransformedDocuments(type: string, documents: any[]) {
switch (type) {
case indexTypes.products:
if (!documents?.length) {
return []
}
const productsTransformer =
this.config_.settings?.[indexTypes.products]?.transformer ??
transformProduct
return documents.map(productsTransformer)
default:
return documents
}
}
}
export default MeiliSearchService

View File

@@ -0,0 +1,33 @@
import { Config, Settings } from "meilisearch"
export const meilisearchErrorCodes = {
INDEX_NOT_FOUND: "index_not_found",
}
export interface MeilisearchPluginOptions {
/**
* Meilisearch client configuration
*/
config: Config
/**
* Index settings
*/
settings?: {
[key: string]: IndexSettings
}
}
export type IndexSettings = {
/**
* Settings specific to the provider. E.g. `searchableAttributes`.
*/
indexSettings: Settings
/**
* Primary key for the index. Used to enforce unique documents in an index. See more in Meilisearch' https://docs.meilisearch.com/learn/core_concepts/primary_key.html.
*/
primaryKey?: string
/**
* Document transformer. Used to transform documents before they are added to the index.
*/
transformer?: (document: any) => any
}

View File

@@ -1,3 +1,5 @@
import { Product } from "@medusajs/medusa"
const variantKeys = [
"sku",
"title",
@@ -7,9 +9,12 @@ const variantKeys = [
"hs_code",
"options",
]
const prefix = `variant`
export const transformProduct = (product) => {
export const transformProduct = (product: Product) => {
let transformedProduct = { ...product } as Record<string, unknown>
const initialObj = variantKeys.reduce((obj, key) => {
obj[`${prefix}_${key}`] = []
return obj
@@ -29,13 +34,20 @@ export const transformProduct = (product) => {
return obj
}, initialObj)
product.type_value = product.type && product.type.value
product.collection_title = product.collection && product.collection.title
product.collection_handle = product.collection && product.collection.handle
product.tags_value = product.tags ? product.tags.map((t) => t.value) : []
transformedProduct.type_value = product.type && product.type.value
transformedProduct.collection_title =
product.collection && product.collection.title
transformedProduct.collection_handle =
product.collection && product.collection.handle
transformedProduct.tags_value = product.tags
? product.tags.map((t) => t.value)
: []
transformedProduct.categories = (product?.categories || []).map(c => c.name)
return {
const prod = {
...product,
...flattenedVariantFields,
}
return prod
}

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"exclude": [
"dist",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -1,4 +1,3 @@
import { TransactionBaseService } from "./transaction-base-service"
import { SearchService } from "medusa-interfaces"
export interface ISearchService {
@@ -72,10 +71,7 @@ export interface ISearchService {
updateSettings(indexName: string, settings: unknown): unknown
}
export abstract class AbstractSearchService
extends TransactionBaseService
implements ISearchService
{
export abstract class AbstractSearchService implements ISearchService {
abstract readonly isDefault
protected readonly options_: Record<string, unknown>
@@ -84,7 +80,6 @@ export abstract class AbstractSearchService
}
protected constructor(container, options) {
super(container, options)
this.options_ = options
}

View File

@@ -10,8 +10,6 @@ type InjectedDependencies = {
export default class DefaultSearchService extends AbstractSearchService {
isDefault = true
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly logger_: Logger
protected readonly options_: Record<string, unknown>
@@ -25,7 +23,6 @@ export default class DefaultSearchService extends AbstractSearchService {
this.options_ = options
this.logger_ = logger
this.manager_ = manager
}
async createIndex(indexName: string, options: unknown): Promise<void> {

View File

@@ -1,29 +1,35 @@
import EventBusService from "../services/event-bus"
import { SEARCH_INDEX_EVENT } from "../loaders/search-index"
import ProductService from "../services/product"
import { indexTypes } from "medusa-core-utils"
import { Product } from "../models"
import { ISearchService } from "../interfaces"
import ProductCategoryFeatureFlag from "../loaders/feature-flags/product-categories"
import { SEARCH_INDEX_EVENT } from "../loaders/search-index"
import { Product } from "../models"
import EventBusService from "../services/event-bus"
import ProductService from "../services/product"
import { FlagRouter } from "../utils/flag-router"
type InjectedDependencies = {
eventBusService: EventBusService
searchService: ISearchService
productService: ProductService
featureFlagRouter: FlagRouter
}
class SearchIndexingSubscriber {
private readonly eventBusService_: EventBusService
private readonly searchService_: ISearchService
private readonly productService_: ProductService
private readonly featureFlagRouter_: FlagRouter
constructor({
eventBusService,
searchService,
productService,
featureFlagRouter,
}: InjectedDependencies) {
this.eventBusService_ = eventBusService
this.searchService_ = searchService
this.productService_ = productService
this.featureFlagRouter_ = featureFlagRouter
this.eventBusService_.subscribe(SEARCH_INDEX_EVENT, this.indexDocuments)
}
@@ -54,36 +60,27 @@ class SearchIndexingSubscriber {
lastSeenId: string,
take: number
): Promise<Product[]> {
const relations = [
"variants",
"tags",
"type",
"collection",
"variants.prices",
"images",
"variants.options",
"options",
]
if (
this.featureFlagRouter_.isFeatureEnabled(ProductCategoryFeatureFlag.key)
) {
relations.push("categories")
}
return await this.productService_.list(
{ id: { gt: lastSeenId } },
{
select: [
"id",
"title",
"status",
"subtitle",
"description",
"handle",
"is_giftcard",
"discountable",
"thumbnail",
"profile_id",
"collection_id",
"type_id",
"origin_country",
"created_at",
"updated_at",
],
relations: [
"variants",
"tags",
"type",
"collection",
"variants.prices",
"images",
"variants.options",
"options",
],
relations,
take: take,
order: { id: "ASC" },
}

View File

@@ -5841,7 +5841,7 @@ __metadata:
languageName: unknown
linkType: soft
"@medusajs/medusa@*, @medusajs/medusa@^1.7.6, @medusajs/medusa@^1.7.7, @medusajs/medusa@^1.7.8, @medusajs/medusa@workspace:packages/medusa":
"@medusajs/medusa@*, @medusajs/medusa@^1.7.12, @medusajs/medusa@^1.7.6, @medusajs/medusa@^1.7.7, @medusajs/medusa@^1.7.8, @medusajs/medusa@workspace:packages/medusa":
version: 0.0.0-use.local
resolution: "@medusajs/medusa@workspace:packages/medusa"
dependencies:
@@ -28729,25 +28729,18 @@ __metadata:
version: 0.0.0-use.local
resolution: "medusa-plugin-meilisearch@workspace:packages/medusa-plugin-meilisearch"
dependencies:
"@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
"@medusajs/medusa": ^1.7.12
body-parser: ^1.19.0
client-sessions: ^0.8.0
cross-env: ^5.2.1
jest: ^25.5.4
lodash: ^4.17.21
medusa-core-utils: ^1.1.39
medusa-interfaces: ^1.3.6
meilisearch: 0.27.0
meilisearch: ^0.31.1
typescript: ^4.9.5
peerDependencies:
medusa-interfaces: 1.3.6
"@medusajs/medusa": ^1.7.12
medusa-interfaces: ^1.3.6
languageName: unknown
linkType: soft
@@ -29027,12 +29020,12 @@ __metadata:
languageName: unknown
linkType: soft
"meilisearch@npm:0.27.0":
version: 0.27.0
resolution: "meilisearch@npm:0.27.0"
"meilisearch@npm:^0.31.1":
version: 0.31.1
resolution: "meilisearch@npm:0.31.1"
dependencies:
cross-fetch: ^3.1.5
checksum: 81903e8e5048eed7f32db0304c9829f4e9ff162a260830675b936f18223ffc0b3ca6b6f1e169563ea3cb5b39adb46a1506d96dc7ad5a1888c3a893e0e415c7ac
checksum: 142f3c401b56723f28376d1e323eeb9bdffecfbf880d239f38c8950663dc97c3f82cd2ef1af0b5e91cb013f63b7fa0628d63980036a1db351a27101097666237
languageName: node
linkType: hard