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
co-authored by adrien2p
parent 4213326fe8
commit 7e17e0ddc2
21 changed files with 377 additions and 217 deletions
@@ -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)
}
}
@@ -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)
}
}
@@ -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
@@ -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
@@ -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
}
@@ -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
}