Merge pull request #381 from medusajs/feat/meilisearch-plugin
feat: meilisearch product plugin
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
|
||||
/packages/medusa/src/subscribers/notification.js
|
||||
/packages/medusa/src/subscribers/order.js
|
||||
/packages/medusa/src/subscribers/product.js
|
||||
|
||||
/packages/medusa/src/loaders/api.js
|
||||
/packages/medusa/src/loaders/database.js
|
||||
@@ -64,6 +65,7 @@
|
||||
/packages/medusa-payment-manual
|
||||
/packages/medusa-payment-paypal
|
||||
/packages/medusa-payment-stripe
|
||||
/packages/medusa-plugin-meilisearch
|
||||
/packages/medusa-plugin-add-ons
|
||||
/packages/medusa-plugin-brightpearl
|
||||
/packages/medusa-plugin-contentful
|
||||
|
||||
3
packages/medusa-core-utils/src/index-types.js
Normal file
3
packages/medusa-core-utils/src/index-types.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const indexTypes = {
|
||||
products: "products",
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { countries } from "./countries"
|
||||
export { isoCountryLookup } from "./countries"
|
||||
export { transformIdableFields } from "./transform-idable-fields"
|
||||
export { indexTypes } from "./index-types"
|
||||
export { default as Validator } from "./validator"
|
||||
export { default as MedusaError } from "./errors"
|
||||
export { default as getConfigFile } from "./get-config-file"
|
||||
|
||||
@@ -19,7 +19,7 @@ class BaseService {
|
||||
* Used to build TypeORM queries.
|
||||
*/
|
||||
buildQuery_(selector, config = {}) {
|
||||
const build = obj => {
|
||||
const build = (obj) => {
|
||||
const where = Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
switch (true) {
|
||||
case value instanceof FindOperator:
|
||||
@@ -49,11 +49,11 @@ class BaseService {
|
||||
})
|
||||
|
||||
acc[key] = Raw(
|
||||
a =>
|
||||
(a) =>
|
||||
subquery
|
||||
.map((s, index) => `${a} ${s.operator} :${index}`)
|
||||
.join(" AND "),
|
||||
subquery.map(s => s.value)
|
||||
subquery.map((s) => s.value)
|
||||
)
|
||||
break
|
||||
default:
|
||||
@@ -149,7 +149,7 @@ class BaseService {
|
||||
return work(this.transactionManager_)
|
||||
} else {
|
||||
const temp = this.manager_
|
||||
const doWork = async m => {
|
||||
const doWork = async (m) => {
|
||||
this.manager_ = m
|
||||
this.transactionManager_ = m
|
||||
try {
|
||||
@@ -167,17 +167,17 @@ class BaseService {
|
||||
if (isolation) {
|
||||
let result
|
||||
try {
|
||||
result = await this.manager_.transaction(isolation, m => doWork(m))
|
||||
result = await this.manager_.transaction(isolation, (m) => doWork(m))
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.shouldRetryTransaction(error)) {
|
||||
return this.manager_.transaction(isolation, m => doWork(m))
|
||||
return this.manager_.transaction(isolation, (m) => doWork(m))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.manager_.transaction(m => doWork(m))
|
||||
return this.manager_.transaction((m) => doWork(m))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ class BaseService {
|
||||
*/
|
||||
runDecorators_(obj, fields = [], expandFields = []) {
|
||||
return this.decorators_.reduce(async (acc, next) => {
|
||||
return acc.then(res => next(res, fields, expandFields)).catch(() => acc)
|
||||
return acc.then((res) => next(res, fields, expandFields)).catch(() => acc)
|
||||
}, Promise.resolve(obj))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@ export { default as FulfillmentService } from "./fulfillment-service"
|
||||
export { default as FileService } from "./file-service"
|
||||
export { default as NotificationService } from "./notification-service"
|
||||
export { default as OauthService } from "./oauth-service"
|
||||
export { default as SearchService } from "./search-service"
|
||||
|
||||
96
packages/medusa-interfaces/src/search-service.js
Normal file
96
packages/medusa-interfaces/src/search-service.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
/**
|
||||
* The interface that all search services must implement.
|
||||
* @interface
|
||||
*/
|
||||
class SearchService extends BaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create an index
|
||||
* @param indexName {string} - the index name
|
||||
* @param [options] {string} - the index name
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
createIndex(indexName, options) {
|
||||
throw Error("createIndex must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to get an index
|
||||
* @param indexName {string} - the index name.
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
getIndex(indexName) {
|
||||
throw Error("getIndex must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to index documents by the search engine provider
|
||||
* @param indexName {string} - the index name
|
||||
* @param documents {Array.<Object>} - documents array to be indexed
|
||||
* @param type {Array.<Object>} - type of documents to be added (e.g: products, regions, orders, etc)
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
addDocuments(indexName, documents, type) {
|
||||
throw Error("addDocuments must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to replace documents
|
||||
* @param indexName {string} - the index name.
|
||||
* @param documents {Object} - array of document objects that will replace existing documents
|
||||
* @param type {Array.<Object>} - type of documents to be replaced (e.g: products, regions, orders, etc)
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
replaceDocuments(indexName, documents, type) {
|
||||
throw Error("updateDocument must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete document
|
||||
* @param indexName {string} - the index name
|
||||
* @param document_id {string} - the id of the document
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
deleteDocument(indexName, document_id) {
|
||||
throw Error("deleteDocument must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to delete all documents
|
||||
* @param indexName {string} - the index name
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
deleteAllDocuments(indexName) {
|
||||
throw Error("deleteAllDocuments must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to search for a document in an index
|
||||
* @param indexName {string} - the index name
|
||||
* @param query {string} - the search query
|
||||
* @param options {{ paginationOptions: { limit: number, offset: number }, filter: any, additionalOptions: any}}
|
||||
* - any options passed to the request object other than the query and indexName
|
||||
* - additionalOptions contain any provider specific options
|
||||
* @return {Promise<{ hits: any[]; [k: string]: any; }>} returns response from search engine provider
|
||||
*/
|
||||
search(indexName, query, options) {
|
||||
throw Error("search must be overridden by a child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to update the settings of an index
|
||||
* @param indexName {string} - the index name
|
||||
* @param settings {object} - settings object
|
||||
* @return {Promise<{object}>} - returns response from search engine provider
|
||||
*/
|
||||
updateSettings(indexName, settings) {
|
||||
throw Error("updateSettings must be overridden by a child class")
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchService
|
||||
13
packages/medusa-plugin-meilisearch/.babelrc
Normal file
13
packages/medusa-plugin-meilisearch/.babelrc
Normal 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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/medusa-plugin-meilisearch/.eslintignore
Normal file
8
packages/medusa-plugin-meilisearch/.eslintignore
Normal file
@@ -0,0 +1,8 @@
|
||||
/src/subscribers
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/loaders
|
||||
/utils
|
||||
15
packages/medusa-plugin-meilisearch/.gitignore
vendored
Normal file
15
packages/medusa-plugin-meilisearch/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/dist
|
||||
.env
|
||||
.DS_Store
|
||||
/uploads
|
||||
/node_modules
|
||||
yarn-error.log
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/loaders
|
||||
/utils
|
||||
13
packages/medusa-plugin-meilisearch/.npmignore
Normal file
13
packages/medusa-plugin-meilisearch/.npmignore
Normal file
@@ -0,0 +1,13 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
src
|
||||
.gitignore
|
||||
.eslintrc
|
||||
.babelrc
|
||||
.prettierrc
|
||||
|
||||
20
packages/medusa-plugin-meilisearch/README.md
Normal file
20
packages/medusa-plugin-meilisearch/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# medusa-plugin-meilisearch
|
||||
|
||||
Meilisearch Plugin for Medusa to search for products.
|
||||
|
||||
## Plugin Options
|
||||
```
|
||||
{
|
||||
config: {
|
||||
host: [your meilisearch host],
|
||||
},
|
||||
settings: {
|
||||
[indexName]: [meilisearch settings passed to meilisearch's `updateSettings()` method]
|
||||
// example
|
||||
products: {
|
||||
searchableAttributes: ["title", "description", "variant_sku", "type_value"],
|
||||
displayedAttributes: ["title", "description", "variant_sku", "type_value"],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
1
packages/medusa-plugin-meilisearch/index.js
Normal file
1
packages/medusa-plugin-meilisearch/index.js
Normal file
@@ -0,0 +1 @@
|
||||
//noop
|
||||
3
packages/medusa-plugin-meilisearch/jest.config.js
Normal file
3
packages/medusa-plugin-meilisearch/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
}
|
||||
45
packages/medusa-plugin-meilisearch/package.json
Normal file
45
packages/medusa-plugin-meilisearch/package.json
Normal 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.23",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
14
packages/medusa-plugin-meilisearch/src/loaders/index.js
Normal file
14
packages/medusa-plugin-meilisearch/src/loaders/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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,69 @@
|
||||
import { SearchService } from "medusa-interfaces"
|
||||
import { MeiliSearch } from "meilisearch"
|
||||
import { indexTypes } from "medusa-core-utils"
|
||||
import { transformProduct } from "../utils/transform-product"
|
||||
|
||||
class MeiliSearchService extends SearchService {
|
||||
constructor(container, options) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.client_ = new MeiliSearch(options.config)
|
||||
}
|
||||
|
||||
createIndex(indexName, options) {
|
||||
return this.client_.createIndex(indexName, options)
|
||||
}
|
||||
|
||||
getIndex(indexName) {
|
||||
return this.client_.index(indexName)
|
||||
}
|
||||
|
||||
addDocuments(indexName, documents, type) {
|
||||
const transformedDocuments = this.getTransformedDocuments(type, documents)
|
||||
return this.client_.index(indexName).addDocuments(transformedDocuments)
|
||||
}
|
||||
|
||||
replaceDocuments(indexName, documents, type) {
|
||||
const transformedDocuments = this.getTransformedDocuments(type, documents)
|
||||
return this.client_.index(indexName).addDocuments(transformedDocuments)
|
||||
}
|
||||
|
||||
deleteDocument(indexName, document_id) {
|
||||
return this.client_.index(indexName).deleteDocument(document_id)
|
||||
}
|
||||
|
||||
deleteAllDocuments(indexName) {
|
||||
return this.client_.index(indexName).deleteAllDocuments()
|
||||
}
|
||||
|
||||
search(indexName, query, options) {
|
||||
const { paginationOptions, filter, additionalOptions } = options
|
||||
return this.client_
|
||||
.index(indexName)
|
||||
.search(query, { filter, ...paginationOptions, ...additionalOptions })
|
||||
}
|
||||
|
||||
updateSettings(indexName, settings) {
|
||||
return 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,41 @@
|
||||
const variantKeys = [
|
||||
"sku",
|
||||
"title",
|
||||
"upc",
|
||||
"ean",
|
||||
"mid_code",
|
||||
"hs_code",
|
||||
"options",
|
||||
]
|
||||
const prefix = `variant`
|
||||
|
||||
export const transformProduct = (product) => {
|
||||
const initialObj = variantKeys.reduce((obj, key) => {
|
||||
obj[`${prefix}_${key}`] = []
|
||||
return obj
|
||||
}, {})
|
||||
initialObj[`${prefix}_options_value`] = []
|
||||
|
||||
const flattenedVariantFields = product.variants.reduce((obj, variant) => {
|
||||
variantKeys.forEach((k) => {
|
||||
if (k === "options" && variant[k]) {
|
||||
const values = variant[k].map((option) => option.value)
|
||||
obj[`${prefix}_options_value`] =
|
||||
obj[`${prefix}_options_value`].concat(values)
|
||||
return
|
||||
}
|
||||
return variant[k] && obj[`${prefix}_${k}`].push(variant[k])
|
||||
})
|
||||
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) : []
|
||||
|
||||
return {
|
||||
...product,
|
||||
...flattenedVariantFields,
|
||||
}
|
||||
}
|
||||
5453
packages/medusa-plugin-meilisearch/yarn.lock
Normal file
5453
packages/medusa-plugin-meilisearch/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ export default app => {
|
||||
app.use("/products", route)
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-products").default))
|
||||
route.post("/search", middlewares.wrap(require("./search").default))
|
||||
route.get("/:id", middlewares.wrap(require("./get-product").default))
|
||||
|
||||
return app
|
||||
|
||||
35
packages/medusa/src/api/routes/store/products/search.js
Normal file
35
packages/medusa/src/api/routes/store/products/search.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import ProductService from "../../../../services/product"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object()
|
||||
.keys({
|
||||
q: Validator.string().required(),
|
||||
offset: Validator.number().optional(),
|
||||
limit: Validator.number().optional(),
|
||||
filter: Validator.any(),
|
||||
})
|
||||
.options({ allowUnknown: true })
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const { q, offset, limit, filter, ...options } = value
|
||||
const paginationOptions = { offset, limit }
|
||||
|
||||
const searchService = req.scope.resolve("searchService")
|
||||
|
||||
const results = await searchService.search(ProductService.IndexName, q, {
|
||||
paginationOptions,
|
||||
filter,
|
||||
additionalOptions: options,
|
||||
})
|
||||
|
||||
res.status(200).send(results)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
71
packages/medusa/src/loaders/search-index.js
Normal file
71
packages/medusa/src/loaders/search-index.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import ProductService from "../services/product"
|
||||
import { indexTypes } from "medusa-core-utils"
|
||||
|
||||
async function loadProductsIntoSearchEngine(container) {
|
||||
const searchService = container.resolve("searchService")
|
||||
const productService = container.resolve("productService")
|
||||
|
||||
const TAKE = 20
|
||||
let hasMore = true
|
||||
|
||||
let lastSeenId = ""
|
||||
|
||||
while (hasMore) {
|
||||
const products = await productService.list(
|
||||
{ id: { gt: lastSeenId } },
|
||||
{
|
||||
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",
|
||||
"variants.prices",
|
||||
"variants.options",
|
||||
"options",
|
||||
],
|
||||
take: TAKE,
|
||||
order: { id: "ASC" },
|
||||
}
|
||||
)
|
||||
|
||||
if (products.length > 0) {
|
||||
await searchService.addDocuments(
|
||||
ProductService.IndexName,
|
||||
products,
|
||||
indexTypes.products
|
||||
)
|
||||
lastSeenId = products[products.length - 1].id
|
||||
} else {
|
||||
hasMore = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
await loadProductsIntoSearchEngine(container)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class OrderRepository extends Repository<Order> {
|
||||
|
||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||
|
||||
return map(entities, e => merge({}, ...entitiesAndRelationsById[e.id]))
|
||||
return map(entities, (e) => merge({}, ...entitiesAndRelationsById[e.id]))
|
||||
}
|
||||
|
||||
public async findOneWithRelations(
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import { flatten, groupBy, map, merge } from "lodash"
|
||||
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
||||
import {
|
||||
OrderByCondition,
|
||||
EntityRepository,
|
||||
FindManyOptions,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { Product } from "../models/product"
|
||||
|
||||
type DefaultWithoutRelations = Omit<FindManyOptions<Product>, "relations">
|
||||
|
||||
type CustomOptions = {
|
||||
where?: DefaultWithoutRelations["where"] & { tags?: string[] }
|
||||
order?: OrderByCondition
|
||||
skip?: number
|
||||
take?: number
|
||||
}
|
||||
|
||||
type FindWithRelationsOptions = CustomOptions
|
||||
|
||||
@EntityRepository(Product)
|
||||
export class ProductRepository extends Repository<Product> {
|
||||
public async findWithRelations(
|
||||
relations: Array<keyof Product> = [],
|
||||
idsOrOptionsWithoutRelations: Omit<
|
||||
FindManyOptions<Product>,
|
||||
"relations"
|
||||
> = {}
|
||||
idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
|
||||
): Promise<Product[]> {
|
||||
let entities
|
||||
let entities: Product[]
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
||||
} else {
|
||||
// Since tags are in a one-to-many realtion they cant be included in a
|
||||
// regular query, to solve this add the join on tags seperately if
|
||||
// Since tags are in a one-to-many realtion they cant be included in a
|
||||
// regular query, to solve this add the join on tags seperately if
|
||||
// the query exists
|
||||
const tags = idsOrOptionsWithoutRelations.where.tags
|
||||
delete idsOrOptionsWithoutRelations.where.tags
|
||||
@@ -25,17 +38,15 @@ export class ProductRepository extends Repository<Product> {
|
||||
.where(idsOrOptionsWithoutRelations.where)
|
||||
.skip(idsOrOptionsWithoutRelations.skip)
|
||||
.take(idsOrOptionsWithoutRelations.take)
|
||||
|
||||
if (tags) {
|
||||
.orderBy(idsOrOptionsWithoutRelations.order)
|
||||
|
||||
if (tags) {
|
||||
qb = qb
|
||||
.leftJoinAndSelect("product.tags", "tags")
|
||||
.andWhere(
|
||||
`tags.id IN (:...ids)`, { ids: tags._value}
|
||||
)
|
||||
.andWhere(`tags.id IN (:...ids)`, { ids: tags._value })
|
||||
}
|
||||
|
||||
entities = await qb
|
||||
.getMany()
|
||||
|
||||
entities = await qb.getMany()
|
||||
}
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
|
||||
|
||||
@@ -802,6 +802,7 @@ describe("ProductVariantService", () => {
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("ironman"),
|
||||
product_id: IdMap.getId("product-test"),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -820,9 +821,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 () => {
|
||||
|
||||
@@ -417,6 +417,7 @@ describe("ProductService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
productRepository,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -429,6 +430,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"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
async create(productOrProductId, variant) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
const variantRepo = manager.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
@@ -148,8 +148,8 @@ class ProductVariantService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
product.options.forEach(option => {
|
||||
if (!variant.options.find(vo => option.id === vo.option_id)) {
|
||||
product.options.forEach((option) => {
|
||||
if (!variant.options.find((vo) => option.id === vo.option_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variant options do not contain value for ${option.title}`
|
||||
@@ -158,10 +158,10 @@ class ProductVariantService extends BaseService {
|
||||
})
|
||||
|
||||
let variantExists = undefined
|
||||
variantExists = product.variants.find(v => {
|
||||
return v.options.every(option => {
|
||||
variantExists = product.variants.find((v) => {
|
||||
return v.options.every((option) => {
|
||||
const variantOption = variant.options.find(
|
||||
o => option.option_id === o.option_id
|
||||
(o) => option.option_id === o.option_id
|
||||
)
|
||||
|
||||
return option.value === variantOption.value
|
||||
@@ -207,6 +207,7 @@ class ProductVariantService extends BaseService {
|
||||
.withTransaction(manager)
|
||||
.emit(ProductVariantService.Events.CREATED, {
|
||||
id: result.id,
|
||||
product_id: result.product_id,
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -219,7 +220,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise}
|
||||
*/
|
||||
async publish(variantId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const variantRepo = manager.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
@@ -234,6 +235,7 @@ class ProductVariantService extends BaseService {
|
||||
.withTransaction(manager)
|
||||
.emit(ProductVariantService.Events.UPDATED, {
|
||||
id: result.id,
|
||||
product_id: result.product_id,
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -250,7 +252,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
async update(variantOrVariantId, update) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const variantRepo = manager.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
@@ -310,6 +312,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
|
||||
@@ -325,7 +328,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async setCurrencyPrice(variantId, price) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
@@ -364,7 +367,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {number} the price specific to the region
|
||||
*/
|
||||
async getRegionPrice(variantId, regionId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
@@ -412,7 +415,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async setRegionPrice(variantId, price) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const moneyAmountRepo = manager.getCustomRepository(
|
||||
this.moneyAmountRepository_
|
||||
)
|
||||
@@ -449,7 +452,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} the result of the update operation.
|
||||
*/
|
||||
async updateOptionValue(variantId, optionId, optionValue) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionValueRepo = manager.getCustomRepository(
|
||||
this.productOptionValueRepository_
|
||||
)
|
||||
@@ -484,7 +487,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} the result of the update operation.
|
||||
*/
|
||||
async addOptionValue(variantId, optionId, optionValue) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionValueRepo = manager.getCustomRepository(
|
||||
this.productOptionValueRepository_
|
||||
)
|
||||
@@ -508,7 +511,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} empty promise
|
||||
*/
|
||||
async deleteOptionValue(variantId, optionId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionValueRepo = manager.getCustomRepository(
|
||||
this.productOptionValueRepository_
|
||||
)
|
||||
@@ -558,7 +561,7 @@ class ProductVariantService extends BaseService {
|
||||
},
|
||||
}
|
||||
|
||||
query.where = qb => {
|
||||
query.where = (qb) => {
|
||||
qb.where(where).andWhere([
|
||||
{ sku: ILike(`%${q}%`) },
|
||||
{ title: ILike(`%${q}%`) },
|
||||
@@ -578,7 +581,7 @@ class ProductVariantService extends BaseService {
|
||||
* @return {Promise} empty promise
|
||||
*/
|
||||
async delete(variantId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const variantRepo = manager.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
)
|
||||
@@ -592,7 +595,8 @@ class ProductVariantService extends BaseService {
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductVariantService.Events.DELETED, {
|
||||
id: variantId,
|
||||
id: variant.id,
|
||||
product_id: variant.product_id,
|
||||
})
|
||||
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Brackets } from "typeorm"
|
||||
* @implements BaseService
|
||||
*/
|
||||
class ProductService extends BaseService {
|
||||
static IndexName = `products`
|
||||
static Events = {
|
||||
UPDATED: "product.updated",
|
||||
CREATED: "product.created",
|
||||
@@ -25,6 +26,7 @@ class ProductService extends BaseService {
|
||||
productTypeRepository,
|
||||
productTagRepository,
|
||||
imageRepository,
|
||||
searchService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -57,6 +59,9 @@ class ProductService extends BaseService {
|
||||
|
||||
/** @private @const {ImageRepository} */
|
||||
this.imageRepository_ = imageRepository
|
||||
|
||||
/** @private @const {SearchService} */
|
||||
this.searchService_ = searchService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -123,7 +128,7 @@ class ProductService extends BaseService {
|
||||
.select(["product.id"])
|
||||
.where(where)
|
||||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
new Brackets((qb) => {
|
||||
qb.where(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`product.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
||||
@@ -135,7 +140,7 @@ class ProductService extends BaseService {
|
||||
|
||||
return productRepo.findWithRelations(
|
||||
rels,
|
||||
raw.map(i => i.id)
|
||||
raw.map((i) => i.id)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,7 +282,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
async create(productObject) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
const optionRepo = manager.getCustomRepository(
|
||||
this.productOptionRepository_
|
||||
@@ -311,7 +316,7 @@ class ProductService extends BaseService {
|
||||
product = await productRepo.save(product)
|
||||
|
||||
product.options = await Promise.all(
|
||||
options.map(async o => {
|
||||
options.map(async (o) => {
|
||||
const res = optionRepo.create({ ...o, product_id: product.id })
|
||||
await optionRepo.save(res)
|
||||
return res
|
||||
@@ -361,7 +366,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
async update(productId, update) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
const productVariantRepo = manager.getCustomRepository(
|
||||
this.productVariantRepository_
|
||||
@@ -371,15 +376,8 @@ class ProductService extends BaseService {
|
||||
relations: ["variants", "tags", "images"],
|
||||
})
|
||||
|
||||
const {
|
||||
variants,
|
||||
metadata,
|
||||
options,
|
||||
images,
|
||||
tags,
|
||||
type,
|
||||
...rest
|
||||
} = update
|
||||
const { variants, metadata, options, images, tags, type, ...rest } =
|
||||
update
|
||||
|
||||
if (!product.thumbnail && !update.thumbnail && images?.length) {
|
||||
product.thumbnail = images[0]
|
||||
@@ -404,7 +402,7 @@ class ProductService extends BaseService {
|
||||
if (variants) {
|
||||
// Iterate product variants and update their properties accordingly
|
||||
for (const variant of product.variants) {
|
||||
const exists = variants.find(v => v.id && variant.id === v.id)
|
||||
const exists = variants.find((v) => v.id && variant.id === v.id)
|
||||
if (!exists) {
|
||||
await productVariantRepo.remove(variant)
|
||||
}
|
||||
@@ -415,7 +413,7 @@ class ProductService extends BaseService {
|
||||
newVariant.variant_rank = i
|
||||
|
||||
if (newVariant.id) {
|
||||
const variant = product.variants.find(v => v.id === newVariant.id)
|
||||
const variant = product.variants.find((v) => v.id === newVariant.id)
|
||||
|
||||
if (!variant) {
|
||||
throw new MedusaError(
|
||||
@@ -466,7 +464,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} empty promise
|
||||
*/
|
||||
async delete(productId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
|
||||
// Should not fail, if product does not exist, since delete is idempotent
|
||||
@@ -495,7 +493,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} the result of the model update operation
|
||||
*/
|
||||
async addOption(productId, optionTitle) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionRepo = manager.getCustomRepository(
|
||||
this.productOptionRepository_
|
||||
)
|
||||
@@ -504,7 +502,7 @@ class ProductService extends BaseService {
|
||||
relations: ["options", "variants"],
|
||||
})
|
||||
|
||||
if (product.options.find(o => o.title === optionTitle)) {
|
||||
if (product.options.find((o) => o.title === optionTitle)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
`An option with the title: ${optionTitle} already exists`
|
||||
@@ -534,7 +532,7 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
async reorderVariants(productId, variantOrder) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
|
||||
const product = await this.retrieve(productId, {
|
||||
@@ -548,8 +546,8 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
product.variants = variantOrder.map(vId => {
|
||||
const variant = product.variants.find(v => v.id === vId)
|
||||
product.variants = variantOrder.map((vId) => {
|
||||
const variant = product.variants.find((v) => v.id === vId)
|
||||
if (!variant) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
@@ -578,7 +576,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async reorderOptions(productId, optionOrder) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||
|
||||
const product = await this.retrieve(productId, { relations: ["options"] })
|
||||
@@ -590,8 +588,8 @@ class ProductService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
product.options = optionOrder.map(oId => {
|
||||
const option = product.options.find(o => o.id === oId)
|
||||
product.options = optionOrder.map((oId) => {
|
||||
const option = product.options.find((o) => o.id === oId)
|
||||
if (!option) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
@@ -619,7 +617,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} the updated product
|
||||
*/
|
||||
async updateOption(productId, optionId, data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionRepo = manager.getCustomRepository(
|
||||
this.productOptionRepository_
|
||||
)
|
||||
@@ -629,7 +627,8 @@ class ProductService extends BaseService {
|
||||
const { title, values } = data
|
||||
|
||||
const optionExists = product.options.some(
|
||||
o => o.title.toUpperCase() === title.toUpperCase() && o.id !== optionId
|
||||
(o) =>
|
||||
o.title.toUpperCase() === title.toUpperCase() && o.id !== optionId
|
||||
)
|
||||
if (optionExists) {
|
||||
throw new MedusaError(
|
||||
@@ -668,7 +667,7 @@ class ProductService extends BaseService {
|
||||
* @return {Promise} the updated product
|
||||
*/
|
||||
async deleteOption(productId, optionId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const productOptionRepo = manager.getCustomRepository(
|
||||
this.productOptionRepository_
|
||||
)
|
||||
@@ -696,17 +695,17 @@ class ProductService extends BaseService {
|
||||
const firstVariant = product.variants[0]
|
||||
|
||||
const valueToMatch = firstVariant.options.find(
|
||||
o => o.option_id === optionId
|
||||
(o) => o.option_id === optionId
|
||||
).value
|
||||
|
||||
const equalsFirst = await Promise.all(
|
||||
product.variants.map(async v => {
|
||||
const option = v.options.find(o => o.option_id === optionId)
|
||||
product.variants.map(async (v) => {
|
||||
const option = v.options.find((o) => o.option_id === optionId)
|
||||
return option.value === valueToMatch
|
||||
})
|
||||
)
|
||||
|
||||
if (!equalsFirst.every(v => v)) {
|
||||
if (!equalsFirst.every((v) => v)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist.`
|
||||
|
||||
66
packages/medusa/src/services/search.js
Normal file
66
packages/medusa/src/services/search.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { SearchService } from "medusa-interfaces"
|
||||
|
||||
/**
|
||||
* Default class that implements SearchService but provides stuv implementation for all methods
|
||||
* @extends 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, type) {
|
||||
this.logger_.warn(
|
||||
"This is an empty method: addDocuments must be overridden by a child class"
|
||||
)
|
||||
}
|
||||
|
||||
replaceDocuments(indexName, documents, type) {
|
||||
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 { hits: [] }
|
||||
}
|
||||
|
||||
updateSettings(indexName, settings) {
|
||||
this.logger_.warn(
|
||||
"This is an empty method: updateSettings must be overridden by a child class"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DefaultSearchService
|
||||
113
packages/medusa/src/subscribers/product.js
Normal file
113
packages/medusa/src/subscribers/product.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import ProductVariantService from "../services/product-variant"
|
||||
import ProductService from "../services/product"
|
||||
import { indexTypes } from "medusa-core-utils"
|
||||
|
||||
const searchFields = [
|
||||
"id",
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"discountable",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"origin_country",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
const searchRelations = [
|
||||
"variants",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
"variants.prices",
|
||||
"variants.options",
|
||||
"options",
|
||||
]
|
||||
|
||||
class ProductSearchSubscriber {
|
||||
constructor({ eventBusService, searchService, productService }) {
|
||||
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],
|
||||
indexTypes.products
|
||||
)
|
||||
}
|
||||
|
||||
retrieveProduct_ = async (product_id) => {
|
||||
return await this.productService_.retrieve(product_id, {
|
||||
select: searchFields,
|
||||
relations: searchRelations,
|
||||
})
|
||||
}
|
||||
|
||||
handleProductUpdate = async (data) => {
|
||||
const product = await this.retrieveProduct_(data.id)
|
||||
await this.meilisearchService_.addDocuments(
|
||||
ProductService.IndexName,
|
||||
[product],
|
||||
indexTypes.products
|
||||
)
|
||||
}
|
||||
|
||||
handleProductDeletion = async (data) => {
|
||||
await this.meilisearchService_.deleteDocument(
|
||||
ProductService.IndexName,
|
||||
data.id
|
||||
)
|
||||
}
|
||||
|
||||
handleProductVariantChange = async (data) => {
|
||||
const product = await this.retrieveProduct_(data.product_id)
|
||||
await this.meilisearchService_.addDocuments(
|
||||
ProductService.IndexName,
|
||||
[product],
|
||||
indexTypes.products
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductSearchSubscriber
|
||||
Reference in New Issue
Block a user