This commit is contained in:
Sebastian Rindom
2020-01-18 15:08:22 +01:00
parent 2d8dd4b0a9
commit c7cf9b8061
58 changed files with 9873 additions and 2 deletions

Submodule packages/medusa deleted from 6b3ad22911

View File

@@ -3,7 +3,11 @@
"version": "1.0.0",
"description": "Stripe Payment provider for Meduas Commerce",
"main": "index.js",
"repository": "",
"repository": {
"type": "git",
"url": "https://github.com/srindom/medusa",
"directory": "packages/medusa-payment-stripe"
},
"author": "Sebastian Rindom",
"license": "AGPL-3.0-or-later",
"devDependencies": {

9
packages/medusa/.babelrc Normal file
View File

@@ -0,0 +1,9 @@
{
"plugins": ["@babel/plugin-proposal-class-properties"],
"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"
}
}

5
packages/medusa/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/dist
node_modules
.DS_store
.env*

View File

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

23
packages/medusa/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Structure
- Models (`/models`)
This is where the data layer lives. Define data models here no logic only schema and data access layer. (Default is MongoDB so we have data access layer defined for us already)
- Services (`/services`)
This is where our business logic lives. Define services that perform calculations, update the data layer, synchronize services, etc.
- Controllers (`/api`)
This is the interface lives. Define how the user interacts with the service layer. Ensure that the user has permission to do what they intend to, authenticate requests, call service layer.
- Jobs (`/jobs`)
This is where background and recurring tasks live. Want to send some data somewhere every night, this would be where to do it. Calls service layer methods and should, like controllers, not contain business logic.
- Subscribers (`/subscribers`)
This is where events live. Want to perform a certain task whenever something else happens, this is where to do it.
# Extending the core
The core will look for files in the folders listed above, and inject the custom code.

View File

@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: "node",
testPathIgnorePatterns: [
"mocks"
]
}

View File

@@ -0,0 +1,10 @@
module.exports = {
plugins: [
{
resolve: `mrbl-payment-stripe`,
options: {
stripeApiKey: "12345",
},
},
],
}

View File

@@ -0,0 +1,57 @@
{
"name": "medusa",
"version": "1.0.0",
"description": "E-commerce for JAMstack",
"main": "dist/app.js",
"repository": {
"type": "git",
"url": "https://github.com/srindom/medusa",
"directory": "packages/medusa"
},
"author": "Sebastian Rindom",
"license": "AGPL-3.0-or-later",
"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-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.5",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.7.6",
"eslint": "^6.7.2",
"jest": "^24.9.0",
"nodemon": "^2.0.1",
"prettier": "^1.19.1"
},
"scripts": {
"start": "nodemon --watch plugins/ --watch src/ --exec babel-node src/app.js",
"build": "babel src -d dist",
"serve": "node dist/app.js",
"test": "jest"
},
"dependencies": {
"@hapi/joi": "^16.1.8",
"awilix": "^4.2.3",
"bcrypt": "^3.0.7",
"body-parser": "^1.19.0",
"bull": "^3.12.1",
"cookie-parser": "^1.4.4",
"core-js": "^3.4.8",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"joi-objectid": "^3.0.1",
"fs-exists-cached": "^1.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.8.0",
"morgan": "^1.9.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"regenerator-runtime": "^0.13.3",
"winston": "^3.2.1"
}
}

View File

@@ -0,0 +1,15 @@
import { Router } from "express"
import admin from "./routes/admin"
import store from "./routes/store"
import users from "./routes/users"
// guaranteed to get dependencies
export default () => {
const app = Router()
users(app)
admin(app)
store(app)
return app
}

View File

@@ -0,0 +1,5 @@
import passport from "passport"
export default () => {
return passport.authenticate("jwt", { session: false })
}

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
import { Router } from "express"
import middlewares from "../../middlewares"
const route = Router()
export default app => {
app.use("/admin", route)
// Unauthenticated routes
// route.use("/auth", require("./auth").default)
// Authenticated routes
route.use(middlewares.authenticate())
route.use("/products", require("./products").default)
route.use("/product-variants", require("./product-variants").default)
return app
}

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
import { Router } from "express"
import productRoutes from './products'
import middlewares from "../../middlewares"
const route = Router()
export default app => {
app.use("/store", route)
productRoutes(route)
return app
}

View File

@@ -0,0 +1,93 @@
import mongoose from "mongoose"
import getProduct from "../get-product"
describe("Get product by id", () => {
const testId = `${mongoose.Types.ObjectId("56cb91bdc3464f14678934ca")}`
const productServiceMock = {
getProduct: jest.fn().mockImplementation(id => {
if (id === testId) {
return Promise.resolve({ _id: id, title: "test" })
}
return Promise.resolve(undefined)
}),
}
const reqMock = id => {
return {
params: {
productId: id,
},
scope: {
resolve: jest.fn().mockImplementation(name => {
if (name === "productService") {
return productServiceMock
}
return undefined
}),
},
}
}
const resMock = {
sendStatus: jest.fn().mockReturnValue(),
json: jest.fn().mockReturnValue(),
}
describe("get product by id successfull", () => {
beforeAll(async () => {
await getProduct(reqMock(testId), resMock)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls get product from productSerice", () => {
expect(productServiceMock.getProduct).toHaveBeenCalledTimes(1)
expect(productServiceMock.getProduct).toHaveBeenCalledWith(testId)
})
it("calls res.json", () => {
expect(resMock.json).toHaveBeenCalledTimes(1)
expect(resMock.json).toHaveBeenCalledWith({
_id: testId,
title: "test",
})
})
})
describe("returns 404 when product not found", () => {
beforeAll(async () => {
const id = mongoose.Types.ObjectId()
await getProduct(reqMock(`${id}`), resMock)
})
afterAll(() => {
jest.clearAllMocks()
})
it("return 404", () => {
expect(resMock.sendStatus).toHaveBeenCalledTimes(1)
expect(resMock.json).toHaveBeenCalledTimes(0)
expect(resMock.sendStatus).toHaveBeenCalledWith(404)
})
})
describe("fails when validation fails", () => {
let res
beforeAll(async () => {
try {
await getProduct(reqMock(`not object id`), resMock)
} catch (err) {
res = err
}
})
afterAll(() => {
jest.clearAllMocks()
})
it("return 404", () => {
expect(res.name).toEqual("ValidationError")
})
})
})

View File

@@ -0,0 +1,22 @@
import validator from "../../../../utils/validator"
export default async (req, res) => {
const { productId } = req.params
const schema = validator.objectId()
const { value, error } = schema.validate(productId)
if (error) {
throw error
}
const productService = req.scope.resolve("productService")
const product = await productService.getProduct(value)
if (!product) {
res.sendStatus(404)
return
}
res.json(product)
}

View File

@@ -0,0 +1,10 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/products", route)
route.get("/:productId", middlewares.wrap(require("./get-product").default))
return app
}

View File

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

View File

@@ -0,0 +1,46 @@
import "core-js/stable"
import "regenerator-runtime/runtime"
import express from "express"
import loaders from "./loaders"
import Logger from "./loaders/logger"
import { MedusaErrorTypes } from "./utils/errors"
const PORT = process.env.PORT || 80
const startServer = async () => {
const app = express()
await loaders({ expressApp: app })
app.use((err, req, res, next) => {
const logger = req.scope.resolve("logger")
logger.error(err.message)
let statusCode = 500
switch (err.name) {
case "ValidationError":
statusCode = 400
break
case MedusaErrorTypes.INVALID_DATA:
statusCode = 400
break
case MedusaErrorTypes.DB_ERROR:
statusCode = 500
break
default:
break
}
res.json(err).status(statusCode)
})
app.listen(PORT, err => {
if (err) {
console.log(err)
return
}
Logger.info(`Server is ready on port: ${PORT}!`)
})
}
startServer()

View File

@@ -0,0 +1,41 @@
import dotenv from "dotenv"
// Set the NODE_ENV to 'development' by default
process.env.NODE_ENV = process.env.NODE_ENV || "development"
const envFound = dotenv.config()
if (!envFound) {
// This error should crash whole process
throw new Error("⚠️ Couldn't find .env file ⚠️")
}
export default {
/**
* Your favorite port
*/
port: parseInt(process.env.PORT, 10),
databaseURL: process.env.MONGODB_URI,
redisURI: process.env.REDIS_URI,
/**
* Your secret sauce
*/
jwtSecret: process.env.JWT_SECRET,
cookieSecret: process.env.COOKIE_SECRET,
/**
* Used by winston logger
*/
logs: {
level: process.env.LOG_LEVEL || "silly",
},
/**
* API configs
*/
api: {
prefix: "/api",
},
}

View File

@@ -0,0 +1,17 @@
import mongoose from "mongoose"
class IdMap {
ids = {}
getId(key) {
if (this.ids[key]) {
return this.ids[key]
}
const id = `${mongoose.Types.ObjectId()}`
this.ids[key] = id
return id
}
}
const instance = new IdMap()
export default instance

View File

@@ -0,0 +1,121 @@
import mongoose from "mongoose"
/**
* Interface for data models. The default data layer uses an internal mongoose
* model and is as such compatible with MongoDB.
* @interface
*/
class BaseModel {
constructor() {
/** @const the underlying mongoose model used for queries */
this.mongooseModel_ = this.createMongooseModel_(this.schema)
}
/**
* Returns the model schema. The child class must implement the static schema
* property.
* @return {string} the models schema
*/
getSchema() {
if (!this.constructor.schema) {
throw new Error("Schema not defined")
}
return this.constructor.schema
}
/**
* Returns the model name. The child class must implement the static modelName
* property.
* @return {string} the name of the model
*/
getModelName() {
if (!this.constructor.modelName) {
throw new Error("Every model must have a static modelName property")
}
return this.constructor.modelName
}
/**
* @private
* Creates a mongoose model based on schema and model name.
* @return {Mongooose.Model} the mongoose model
*/
createMongooseModel_() {
return mongoose.model(this.getModelName(), this.getSchema())
}
/**
* Queries the mongoose model via the mongoose's findOne.
* @param query {object} a mongoose selector query
* @param options {?object=} mongoose options
* @return {?mongoose.Document} the retreived mongoose document or null.
*/
findOne(query, options = {}) {
return this.mongooseModel_.findOne(query, options)
}
/**
* Queries the mongoose model via the mongoose's find.
* @param query {object} a mongoose selector query
* @param options {?object=} mongoose options
* @return {Array<mongoose.Document>} the retreived mongoose documents or
* an empty array
*/
find(query, options) {
return this.mongooseModel_.find(query, options)
}
/**
* Update a model via the mongoose model's updateOne.
* @param query {object} a mongoose selector query
* @param update {object} mongoose update object
* @param options {?object=} mongoose options
* @return {object} mongoose result
*/
updateOne(query, update, options) {
return this.mongooseModel_.updateOne(query, update, options)
}
/**
* Update a model via the mongoose model's update.
* @param query {object} a mongoose selector query
* @param update {object} mongoose update object
* @param options {?object=} mongoose options
* @return {object} mongoose result
*/
update(query, update, options) {
return this.mongooseModel_.update(query, update, options)
}
/**
* Creates a document in the mongoose model's collection via create.
* @param object {object} the value of the document to be created
* @param options {?object=} mongoose options
* @return {object} mongoose result
*/
create(object, options) {
return this.mongooseModel_.create(object, options)
}
/**
* Deletes a document in the mongoose model's collection
* @param query {object} the value of the document to be created
* @param options {?object=} mongoose options
* @return {object} mongoose result
*/
deleteOne(query, options) {
return this.mongooseModel_.deleteOne(query, options)
}
/**
* Deletes many document in the mongoose model's collection
* @param query {object} the value of the document to be created
* @param options {?object=} mongoose options
* @return {object} mongoose result
*/
delete(query, options) {
return this.mongooseModel_.deleteMany(query, options)
}
}
export default BaseModel

View File

@@ -0,0 +1,6 @@
/**
* Common functionality for Services
* @interface
*/
class BaseService {}
export default BaseService

View File

@@ -0,0 +1,2 @@
export { default as BaseService } from "./base-service"
export { default as BaseModel } from "./base-model"

View File

@@ -0,0 +1,62 @@
import BaseService from "./base-service"
/**
* The interface that all payment services must inherit from. The intercace
* provides the necessary methods for creating, authorizing and managing
* payments.
* @interface
*/
class BasePaymentService extends BaseService {
constructor() {
super()
}
/**
* Used to create a payment to be processed with the service's payment gateway.
* @param cart {object} - the cart that the payment should cover.
* @return {Promise<{object}>} - returns a promise that resolves to an object
* containing the payment data. This data will be saved to the cart for later
* use.
*/
createPayment(cart) {
throw Error("createPayment must be overridden by the child class")
}
/**
* Used to retrieve a payment.
* @param cart {object} - the cart whose payment should be retrieved.
* @return {Promise<{object}>} - returns a promise that resolves to the
* payment object as stored with the provider.
*/
retrievePayment(cart) {
throw Error("getPayment must be overridden by the child class")
}
/**
* Used to update a payment. This method is called when the cart is updated.
* @param cart {object} - the cart whose payment should be updated.
* @return {Promise<{object}>} - returns a promise that resolves to the
* payment object as stored with the provider.
*/
updatePayment(cart) {
throw Error("updatePayment must be overridden by the child class")
}
authorizePayment() {
throw Error("authorizePayment must be overridden by the child class")
}
capturePayment() {
throw Error("capturePayment must be overridden by the child class")
}
refundPayment() {
throw Error("refundPayment must be overridden by the child class")
}
deletePayment() {
throw Error("deletePayment must be overridden by the child class")
}
}
export default BasePaymentService

View File

@@ -0,0 +1,6 @@
import routes from "../api"
export default async ({ app }) => {
app.use("/", routes())
return app
}

View File

@@ -0,0 +1,31 @@
import express from "express"
import bodyParser from "body-parser"
import session from "express-session"
import cookieParser from "cookie-parser"
import cors from "cors"
import morgan from "morgan"
import config from "../config"
export default async ({ app }) => {
app.enable("trust proxy")
app.use(cors())
app.use(morgan("combined"))
app.use(cookieParser())
app.use(bodyParser.json())
app.use(
session({
secret: config.cookieSecret,
resave: false,
saveUninitialized: true,
cookie: { secure: true },
})
)
app.get("/health", (req, res) => {
res.status(200).send("OK")
})
return app
}

View File

@@ -0,0 +1,65 @@
import { createContainer, asValue } from "awilix"
import expressLoader from "./express"
import mongooseLoader from "./mongoose"
import apiLoader from "./api"
import modelsLoader from "./models"
import servicesLoader from "./services"
import passportLoader from "./passport"
import pluginsLoader from "./plugins"
import Logger from "./logger"
export default async ({ expressApp }) => {
const container = createContainer()
container.registerAdd = function(name, registration) {
let storeKey = name + "_STORE"
if (this.registrations[storeKey] === undefined) {
this.register(storeKey, asValue([]))
}
let store = this.resolve(storeKey)
if (this.registrations[name] === undefined) {
this.register(name, asArray(store))
}
store.unshift(registration)
return this
}.bind(container)
container.register({
logger: asValue(Logger),
})
await modelsLoader({ container })
Logger.info("Models initialized")
await servicesLoader({ container })
Logger.info("Services initialized")
await pluginsLoader({ container })
Logger.info("Plugins Intialized")
await mongooseLoader()
Logger.info("MongoDB Intialized")
await expressLoader({ app: expressApp })
Logger.info("Express Intialized")
await passportLoader({ app: expressApp, container })
Logger.info("Passport initialized")
// Add the registered services to the request scope
expressApp.use((req, res, next) => {
req.scope = container.createScope()
next()
})
await apiLoader({ app: expressApp })
Logger.info("API initialized")
}
function asArray(resolvers) {
return {
resolve: (container, opts) => resolvers.map(r => container.build(r, opts)),
}
}

View File

@@ -0,0 +1,32 @@
import winston from "winston"
import config from "../config"
const transports = []
if (process.env.NODE_ENV !== "development") {
transports.push(new winston.transports.Console())
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat()
),
})
)
}
const LoggerInstance = winston.createLogger({
level: config.logs.level,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: "YYYY-MM-DD HH:mm:ss",
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
transports,
})
export default LoggerInstance

View File

@@ -0,0 +1,28 @@
import { Lifetime } from "awilix"
/**
* Registers all models in the model directory
*/
export default ({ container }) => {
// service/auth.js -> authService
container.loadModules(["src/models/*.js"], {
resolverOptions: {
lifetime: Lifetime.SINGLETON,
},
formatName: (rawname, descriptor) => {
const parts = rawname.split("-").map((n, index) => {
if (index !== 0) {
return n.charAt(0).toUpperCase() + n.slice(1)
}
return n
})
const name = parts.join("")
const splat = descriptor.path.split("/")
const namespace = splat[splat.length - 2]
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.slice(1, -1)
return name + upperNamespace
},
})
}

View File

@@ -0,0 +1,11 @@
import mongoose from "mongoose"
import config from "../config"
export default async () => {
const connection = await mongoose.connect(config.databaseURL, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
})
return connection.connection.db
}

View File

@@ -0,0 +1,45 @@
import passport from "passport"
import { Strategy as LocalStrategy } from "passport-local"
import { Strategy as JWTStrategy } from "passport-jwt"
import config from "../config"
export default async ({ app, container }) => {
const authService = container.cradle.authService
passport.use(
new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
},
async (email, password, done) => {
try {
const { success, user } = authService.authenticate(email, password)
if (success) {
return done(null, user)
} else {
return done("Incorrect Username / Password")
}
} catch (error) {
return done(error)
}
}
)
)
passport.use(
new JWTStrategy(
{
jwtFromRequest: req => req.cookies.jwt,
secretOrKey: config.jwtSecret,
},
(jwtPayload, done) => {
if (Date.now() > jwtPayload.expires) {
return done("jwt expired")
}
return done(null, jwtPayload)
}
)
)
}

View File

@@ -0,0 +1,201 @@
import glob from "glob"
import _ from "lodash"
import path from "path"
import fs from "fs"
import { asFunction } from "awilix"
import { sync as existsSync } from "fs-exists-cached"
import { plugins } from "../../medusa-config.js"
import PaymentService from "../interfaces/payment-service"
import BaseModel from "../interfaces/base-model"
import BaseService from "../interfaces/base-service"
/**
* Registers all services in the services directory
*/
export default ({ container }) => {
const resolved = plugins.map(plugin => {
if (_.isString(plugin)) {
return resolvePlugin(plugin)
}
const details = resolvePlugin(plugin.resolve)
details.options = plugin.options
return details
})
resolved.forEach(pluginDetails => {
registerServices(pluginDetails, container)
registerModels(pluginDetails, container)
})
}
/**
* Registers a service at the right location in our container. If the service is
* a BaseService instance it will be available directly from the container.
* PaymentService instances are added to the paymentProviders array in the
* container. Names are camelCase formatted and namespaced by the folder i.e:
* services/example-payments -> examplePaymentsService
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerServices(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/services/*`, {})
files.forEach(fn => {
const loaded = require(fn).default
if (!(loaded.prototype instanceof BaseService)) {
const logger = container.resolve("logger")
const message = `Models must inherit from BaseModel, please check ${fn}`
logger.error(message)
throw new Error(message)
}
if (loaded.prototype instanceof PaymentService) {
// Register our payment providers to paymentProviders
container.registerAdd(
"paymentProviders",
asFunction(cradle => new loaded(cradle, pluginDetails.options))
)
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[`pp_${loaded.identifier}`]: asFunction(
cradle => new loaded(cradle, pluginDetails.options)
),
})
} else {
const name = formatRegistrationName(fn)
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
})
}
})
}
/**
* Registers a plugin's models at the right location in our container. Models
* must inherit from BaseModel. Models are registered directly in the container.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerModels(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/models/*`, {})
files.forEach(fn => {
const loaded = require(fn).default
if (!(loaded.prototype instanceof BaseModel)) {
const logger = container.resolve("logger")
const message = `Models must inherit from BaseModel, please check ${fn}`
logger.error(message)
throw new Error(message)
}
const name = formatRegistrationName(fn)
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
})
})
}
/**
* Formats a filename into the correct container resolution name.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {string} fn - the full path of the file
* @return {string} the formatted name
*/
function formatRegistrationName(fn) {
const descriptor = fn.split(".")[0]
const splat = descriptor.split("/")
const rawname = splat[splat.length - 1]
const namespace = splat[splat.length - 2]
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.slice(1, -1)
const parts = rawname.split("-").map((n, index) => {
if (index !== 0) {
return n.charAt(0).toUpperCase() + n.slice(1)
}
return n
})
return parts.join("") + upperNamespace
}
// TODO: Create unique id for each plugin
function createPluginId(name) {
return name
}
/**
* Finds the correct path for the plugin. If it is a local plugin it will be
* found in the plugins folder. Otherwise we will look for the plugin in the
* installed npm packages.
* @param {string} pluginName - the name of the plugin to find. Should match
* the name of the folder where the plugin is contained.
* @return {object} the plugin details
*/
function resolvePlugin(pluginName) {
// Only find plugins when we're not given an absolute path
if (!existsSync(pluginName)) {
// Find the plugin in the local plugins folder
const resolvedPath = path.resolve(`./plugins/${pluginName}`)
if (existsSync(resolvedPath)) {
if (existsSync(`${resolvedPath}/package.json`)) {
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
const name = packageJSON.name || pluginName
//warnOnIncompatiblePeerDependency(name, packageJSON)
return {
resolve: resolvedPath,
name,
id: createPluginId(name),
options: {},
version:
packageJSON.version || createFileContentHash(resolvedPath, `**`),
}
} else {
// Make package.json a requirement for local plugins too
throw new Error(`Plugin ${pluginName} requires a package.json file`)
}
}
}
/**
* Here we have an absolute path to an internal plugin, or a name of a module
* which should be located in node_modules.
*/
try {
// If the path is absolute, resolve the directory of the internal plugin,
// otherwise resolve the directory containing the package.json
const resolvedPath = path.dirname(
require.resolve(`${pluginName}/package.json`)
)
const packageJSON = JSON.parse(
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
// warnOnIncompatiblePeerDependency(packageJSON.name, packageJSON)
return {
resolve: resolvedPath,
id: createPluginId(packageJSON.name),
name: packageJSON.name,
version: packageJSON.version,
}
} catch (err) {
throw new Error(
`Unable to find plugin "${pluginName}". Perhaps you need to install its package?`
)
}
}

View File

@@ -0,0 +1,28 @@
import { Lifetime } from "awilix"
/**
* Registers all services in the services directory
*/
export default ({ container }) => {
// service/auth.js -> authService
container.loadModules(["src/services/*.js"], {
resolverOptions: {
lifetime: Lifetime.SINGLETON,
},
formatName: (rawname, descriptor) => {
const parts = rawname.split("-").map((n, index) => {
if (index !== 0) {
return n.charAt(0).toUpperCase() + n.slice(1)
}
return n
})
const name = parts.join("")
const splat = descriptor.path.split("/")
const namespace = splat[splat.length - 2]
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.slice(1, -1)
return name + upperNamespace
},
})
}

View File

@@ -0,0 +1,20 @@
/*******************************************************************************
* models/cart-item.js
*
******************************************************************************/
import { BaseModel } from "../interfaces"
class CartItemModel extends BaseModel {
static modelName = "CartItem"
static schema = {
type: {
type: String,
enum: ["product", "bundle"],
default: "product",
required: true,
},
quantity: { type: Number, min: 0, required: true },
}
}
export default CartItemModel

View File

@@ -0,0 +1,21 @@
/*******************************************************************************
* models/product-variant.js
*
******************************************************************************/
import mongoose from "mongoose"
import { BaseModel } from "../interfaces"
import MoneyAmountSchema from "./schemas/money-amount"
import OptionValueSchema from "./schemas/option-value"
class ProductVariantModel extends BaseModel {
static modelName = "ProductVariant"
static schema = {
title: { type: String, required: true },
prices: { type: [MoneyAmountSchema], default: [], required: true },
options: { type: [OptionValueSchema], default: [] },
image: { type: String, default: "" },
}
}
export default ProductVariantModel

View File

@@ -0,0 +1,25 @@
/*******************************************************************************
* models/product.js
*
******************************************************************************/
import mongoose from "mongoose"
import { BaseModel } from "../interfaces"
import OptionSchema from "./schemas/option"
class ProductModel extends BaseModel {
static modelName = "Product"
static schema = {
title: { type: String, required: true },
description: { type: String, default: "" },
tags: { type: String, default: "" },
handle: { type: String, required: true, unique: true },
images: { type: [String], default: [] },
options: { type: [OptionSchema], default: [] },
variants: { type: [String], default: [] },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
published: { type: Boolean, default: false },
}
}
export default ProductModel

View File

@@ -0,0 +1,10 @@
/*******************************************************************************
* models/money-amount.js
*
******************************************************************************/
import mongoose from "mongoose"
export default new mongoose.Schema({
currency_code: { type: String, required: true },
amount: { type: Number, required: true, min: 0 },
})

View File

@@ -0,0 +1,6 @@
import mongoose from "mongoose"
export default new mongoose.Schema({
option_id: { type: mongoose.Types.ObjectId, required: true },
value: { type: String, required: true },
})

View File

@@ -0,0 +1,11 @@
/*******************************************************************************
* models/option.js
*
******************************************************************************/
import mongoose from "mongoose"
export default new mongoose.Schema({
product_id: { type: mongoose.Types.ObjectId, required: true },
title: { type: String, required: true },
values: { type: [String], default: [] },
})

View File

@@ -0,0 +1,16 @@
/*******************************************************************************
* models/user.js
*
******************************************************************************/
import { BaseModel } from "../interfaces"
class UserModel extends BaseModel {
static modelName = "User"
static schema = {
name: { type: String, required: true },
email: { type: String, required: true },
passwordHash: { type: String, required: true },
}
}
export default UserModel

View File

@@ -0,0 +1,44 @@
import bcrypt from "bcrypt"
import AuthService from "../auth"
const UserModelMock = {
findOne: opt => {
return bcrypt
.hash("123456", 10)
.then(hash => ({ email: "email@mail.com", passwordHash: hash }))
},
}
describe("AuthService", () => {
describe("constructor", () => {
let authService
beforeAll(() => {
authService = new AuthService({ userModel: UserModelMock })
})
it("assigns userModel", () => {
expect(authService.userModel_).toEqual(UserModelMock)
})
})
describe("authenticate", () => {
let authService
beforeEach(() => {
authService = new AuthService({ userModel: UserModelMock })
})
it("returns success when passwords match", async () => {
const result = await authService.authenticate("email@mail.com", "123456")
expect(result.success).toEqual(true)
expect(result.user.email).toEqual("email@mail.com")
})
it("returns failure when passwords don't match", async () => {
const result = await authService.authenticate("email@mail.com", "not")
expect(result.success).toEqual(false)
expect(result.user).toEqual(undefined)
})
})
})

View File

@@ -0,0 +1,136 @@
import Bull from "bull"
import EventBusService from "../event-bus"
import config from "../../config"
jest.genMockFromModule("bull")
jest.mock("bull")
jest.mock("../../config")
config.redisURI = "testhost"
const loggerMock = {
info: jest.fn().mockReturnValue(console.log),
warn: jest.fn().mockReturnValue(console.log),
error: jest.fn().mockReturnValue(console.log),
}
describe("EventBusService", () => {
describe("constructor", () => {
beforeAll(() => {
jest.resetAllMocks()
const eventBus = new EventBusService({ logger: loggerMock })
})
it("creates bull queue", () => {
expect(Bull).toHaveBeenCalledTimes(1)
expect(Bull).toHaveBeenCalledWith("EventBusService:queue", "testhost")
})
})
describe("subscribe", () => {
let eventBus
describe("successfully adds subscriber", () => {
beforeAll(() => {
jest.resetAllMocks()
eventBus = new EventBusService({ logger: loggerMock })
eventBus.subscribe("eventName", () => "test")
})
it("added the subscriber to the queue", () => {
expect(eventBus.observers_["eventName"].length).toEqual(1)
})
})
describe("fails when adding non-function subscriber", () => {
beforeAll(() => {
jest.resetAllMocks()
eventBus = new EventBusService({ logger: loggerMock })
})
it("rejects subscriber with error", () => {
try {
eventBus.subscribe("eventName", 1234)
} catch (err) {
expect(err.message).toEqual("Subscriber must be a function")
}
})
})
})
describe("emit", () => {
let eventBus, job
describe("successfully adds job to queue", () => {
beforeAll(() => {
jest.resetAllMocks()
eventBus = new EventBusService({ logger: loggerMock })
eventBus.queue_.add.mockImplementationOnce(() => "hi")
job = eventBus.emit("eventName", { hi: "1234" })
})
it("calls queue.add", () => {
expect(eventBus.queue_.add).toHaveBeenCalled()
})
it("returns the job", () => {
expect(job).toEqual("hi")
})
})
})
describe("worker", () => {
let eventBus, result
describe("successfully runs the worker", () => {
beforeAll(async () => {
jest.resetAllMocks()
eventBus = new EventBusService({ logger: loggerMock })
eventBus.subscribe("eventName", () => Promise.resolve("hi"))
result = await eventBus.worker_({ eventName: "eventName", data: {} })
})
it("calls logger", () => {
expect(loggerMock.info).toHaveBeenCalled()
expect(loggerMock.info).toHaveBeenCalledWith(
"Processing eventName which has 1 subscribers"
)
})
it("returns array with hi", async () => {
expect(result).toEqual(["hi"])
})
})
describe("continue if errors occur", () => {
beforeAll(async () => {
jest.resetAllMocks()
eventBus = new EventBusService({ logger: loggerMock })
eventBus.subscribe("eventName", () => Promise.resolve("hi"))
eventBus.subscribe("eventName", () => Promise.resolve("hi2"))
eventBus.subscribe("eventName", () => Promise.resolve("hi3"))
eventBus.subscribe("eventName", () => Promise.reject("fail1"))
eventBus.subscribe("eventName", () => Promise.reject("fail2"))
eventBus.subscribe("eventName", () => Promise.reject("fail3"))
result = await eventBus.worker_({ eventName: "eventName", data: {} })
})
it("calls logger warn on rejections", () => {
expect(loggerMock.warn).toHaveBeenCalledTimes(3)
expect(loggerMock.warn).toHaveBeenCalledWith(
"An error occured while processing eventName: fail1"
)
expect(loggerMock.warn).toHaveBeenCalledWith(
"An error occured while processing eventName: fail2"
)
expect(loggerMock.warn).toHaveBeenCalledWith(
"An error occured while processing eventName: fail3"
)
})
it("returns result from all subscribers", async () => {
expect(result.length).toEqual(6)
})
})
})
})

View File

@@ -0,0 +1,110 @@
import IdMap from "../../../helpers/id-map"
export const ProductModelMock = {
create: jest.fn().mockReturnValue(Promise.resolve()),
updateOne: jest.fn().mockImplementation((query, update) => {
if (query._id === IdMap.getId("productWithVariantsFail")) {
return Promise.reject()
}
return Promise.resolve()
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query._id === IdMap.getId("productWithFourVariants")) {
return Promise.resolve({
_id: IdMap.getId("productWithFourVariants"),
title: "Product With Variants",
variants: ["1", "2", "3", "4"],
options: [
{
_id: IdMap.getId("color_id"),
title: "Color",
},
{
_id: IdMap.getId("size_id"),
title: "Size",
},
],
})
}
if (query._id === IdMap.getId("productWithVariantsFail")) {
return Promise.resolve({
_id: IdMap.getId("productWithVariantsFail"),
title: "Product With Variants",
variants: ["1", "3", "4"],
options: [
{
_id: IdMap.getId("color_id"),
title: "Color",
},
{
_id: IdMap.getId("size_id"),
title: "Size",
},
],
})
}
if (query._id === IdMap.getId("productWithVariants")) {
return Promise.resolve({
_id: IdMap.getId("productWithVariants"),
title: "Product With Variants",
variants: ["1", "3", "4"],
options: [
{
_id: IdMap.getId("color_id"),
title: "Color",
},
{
_id: IdMap.getId("size_id"),
title: "Size",
},
],
})
}
if (query._id === IdMap.getId("variantProductId")) {
return Promise.resolve({
_id: IdMap.getId("variantProductId"),
title: "testtitle",
options: [
{
_id: IdMap.getId("color_id"),
title: "Color",
},
{
_id: IdMap.getId("size_id"),
title: "Size",
},
],
})
}
if (query._id === IdMap.getId("emptyVariantProductId")) {
return Promise.resolve({
_id: IdMap.getId("emptyVariantProductId"),
title: "testtitle",
options: [],
})
}
if (query._id === IdMap.getId("deleteId")) {
return Promise.resolve({
_id: IdMap.getId("deleteId"),
variants: ["1", "2"],
})
}
if (query._id === IdMap.getId("validId")) {
return Promise.resolve({
_id: IdMap.getId("validId"),
title: "test",
})
}
if (query._id === IdMap.getId("failId")) {
return Promise.reject(new Error("test error"))
}
return Promise.resolve(undefined)
}),
}

View File

@@ -0,0 +1,122 @@
import IdMap from "../../../helpers/id-map"
const variant1 = {
_id: "1",
title: "variant1",
options: [
{
option_id: IdMap.getId("color_id"),
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "160",
},
],
}
const variant2 = {
_id: "2",
title: "variant2",
options: [
{
option_id: IdMap.getId("color_id"),
value: "black",
},
{
option_id: IdMap.getId("size_id"),
value: "160",
},
],
}
const variant3 = {
_id: "3",
title: "variant3",
options: [
{
option_id: IdMap.getId("color_id"),
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "150",
},
],
}
const variant4 = {
_id: "4",
title: "variant4",
options: [
{
option_id: IdMap.getId("color_id"),
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "50",
},
],
}
const invalidVariant = {
_id: "invalid_option",
title: "variant3",
options: [
{
option_id: "invalid_id",
value: "blue",
},
{
option_id: IdMap.getId("size_id"),
value: "150",
},
],
}
const emptyVariant = {
_id: "empty_option",
title: "variant3",
options: [],
}
export const variants = {
one: variant1,
two: variant2,
three: variant3,
four: variant4,
invalid_variant: invalidVariant,
empty_variant: emptyVariant,
}
export const ProductVariantServiceMock = {
retrieve: jest.fn().mockImplementation(variantId => {
if (variantId === "1") {
return Promise.resolve(variant1)
}
if (variantId === "2") {
return Promise.resolve(variant2)
}
if (variantId === "3") {
return Promise.resolve(variant3)
}
if (variantId === "4") {
return Promise.resolve(variant4)
}
if (variantId === "invalid_option") {
return Promise.resolve(invalidVariant)
}
if (variantId === "empty_option") {
return Promise.resolve(emptyVariant)
}
return Promise.resolve(undefined)
}),
delete: jest.fn().mockReturnValue(Promise.resolve()),
addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => {
return Promise.resolve({})
}),
deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => {
return Promise.resolve({})
}),
}

View File

@@ -0,0 +1,758 @@
import mongoose from "mongoose"
import ProductService from "../product"
import { ProductModelMock } from "./mocks/product-model"
import {
ProductVariantServiceMock,
variants,
} from "./mocks/product-variant-service"
import IdMap from "../../helpers/id-map"
describe("ProductService", () => {
describe("retrieve", () => {
describe("successfully get product", () => {
let res
beforeAll(async () => {
const productService = new ProductService({
productModel: ProductModelMock,
})
res = await productService.retrieve(IdMap.getId("validId"))
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls model layer findOne", () => {
expect(ProductModelMock.findOne).toHaveBeenCalledTimes(1)
expect(ProductModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("validId"),
})
})
it("returns correct product", () => {
expect(res.title).toEqual("test")
})
})
describe("query fail", () => {
let res
beforeAll(async () => {
const productService = new ProductService({
productModel: ProductModelMock,
})
await productService.retrieve(IdMap.getId("failId")).catch(err => {
res = err
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls model layer findOne", () => {
expect(ProductModelMock.findOne).toHaveBeenCalledTimes(1)
expect(ProductModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("failId"),
})
})
it("model query throws error", () => {
expect(res.name).toEqual("database_error")
expect(res.message).toEqual("test error")
})
})
})
describe("createDraft", () => {
beforeAll(() => {
jest.clearAllMocks()
const productService = new ProductService({
productModel: ProductModelMock,
})
productService.createDraft({
title: "Test Prod",
description: "Test Descript",
tags: "Teststst",
handle: "1234",
images: [],
options: [],
variants: [],
metadata: {},
})
})
it("calls model layer create", () => {
expect(ProductModelMock.create).toHaveBeenCalledTimes(1)
expect(ProductModelMock.create).toHaveBeenCalledWith({
title: "Test Prod",
description: "Test Descript",
tags: "Teststst",
handle: "1234",
images: [],
options: [],
variants: [],
metadata: {},
published: false,
})
})
})
describe("publishProduct", () => {
const productId = mongoose.Types.ObjectId()
beforeAll(() => {
jest.clearAllMocks()
const productService = new ProductService({
productModel: ProductModelMock,
})
productService.publish(IdMap.getId("productId"))
})
it("calls model layer create", () => {
expect(ProductModelMock.create).toHaveBeenCalledTimes(0)
expect(ProductModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(ProductModelMock.updateOne).toHaveBeenCalledWith(
{ _id: IdMap.getId("productId") },
{ $set: { published: true } }
)
})
})
describe("decorate", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
const fakeProduct = {
_id: "1234",
variants: ["1", "2", "3"],
tags: "testtag1, testtag2",
handle: "test-product",
metadata: { testKey: "testValue" },
}
beforeEach(() => {
jest.clearAllMocks()
})
it("returns decorated product", async () => {
const decorated = await productService.decorate(
fakeProduct,
[],
["variants"]
)
expect(decorated).toEqual({
_id: "1234",
metadata: { testKey: "testValue" },
variants: [variants.one, variants.two, variants.three],
})
})
it("returns decorated product with handle", async () => {
const decorated = await productService.decorate(
fakeProduct,
["handle"],
["variants"]
)
expect(decorated).toEqual({
_id: "1234",
metadata: { testKey: "testValue" },
handle: "test-product",
variants: [variants.one, variants.two, variants.three],
})
})
it("returns decorated product with handle and tags", async () => {
const decorated = await productService.decorate(fakeProduct, [
"handle",
"tags",
])
expect(decorated).toEqual({
_id: "1234",
metadata: { testKey: "testValue" },
tags: "testtag1, testtag2",
handle: "test-product",
})
})
it("returns decorated product with metadata", async () => {
const decorated = await productService.decorate(fakeProduct, [])
expect(decorated).toEqual({
_id: "1234",
metadata: { testKey: "testValue" },
})
})
})
describe("add metadata to product model", () => {
const productService = new ProductService({
productModel: ProductModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("calls updateOne with correct params", async () => {
const id = mongoose.Types.ObjectId()
await productService.setMetadata(`${id}`, "metadata", "testMetadata")
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: `${id}` },
{ $set: { "metadata.metadata": "testMetadata" } }
)
})
it("throw error on invalid key type", async () => {
const id = mongoose.Types.ObjectId()
try {
await productService.setMetadata(`${id}`, 1234, "nono")
} catch (err) {
expect(err.message).toEqual(
"Key type is invalid. Metadata keys must be strings"
)
}
})
it("throws error on invalid productId type", async () => {
try {
await productService.setMetadata("fakeProductId", 1234, "nono")
} catch (err) {
expect(err.message).toEqual(
"The productId could not be casted to an ObjectId"
)
}
})
})
describe("update product", () => {
const productService = new ProductService({
productModel: ProductModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("calls updateOne with correct params", async () => {
const id = mongoose.Types.ObjectId()
await productService.update(`${id}`, { title: "new title" })
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: `${id}` },
{ $set: { title: "new title" } },
{ runValidators: true }
)
})
it("throw error on invalid product id type", async () => {
try {
await productService.update(19314235, { title: "new title" })
} catch (err) {
expect(err.message).toEqual(
"The productId could not be casted to an ObjectId"
)
}
})
it("throws error when trying to update metadata", async () => {
const id = mongoose.Types.ObjectId()
try {
await productService.update(`${id}`, { metadata: { key: "value" } })
} catch (err) {
expect(err.message).toEqual("Use setMetadata to update metadata fields")
}
})
it("throws error when trying to update variants", async () => {
const id = mongoose.Types.ObjectId()
try {
await productService.update(`${id}`, { variants: ["1", "2"] })
} catch (err) {
expect(err.message).toEqual(
"Use addVariant, reorderVariants, removeVariant to update Product Variants"
)
}
})
})
describe("delete product", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("deletes all variants and product successfully", async () => {
await productService.delete(IdMap.getId("deleteId"))
expect(ProductVariantServiceMock.delete).toBeCalledTimes(2)
expect(ProductVariantServiceMock.delete).toBeCalledWith("1")
expect(ProductVariantServiceMock.delete).toBeCalledWith("2")
expect(ProductModelMock.deleteOne).toBeCalledTimes(1)
expect(ProductModelMock.deleteOne).toBeCalledWith({
_id: IdMap.getId("deleteId"),
})
})
it("throw error on invalid product id type", async () => {
try {
await productService.update(19314235, { title: "new title" })
} catch (err) {
expect(err.message).toEqual(
"The productId could not be casted to an ObjectId"
)
}
})
})
describe("addVariant", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("add variant to product successfilly", async () => {
await productService.addVariant(IdMap.getId("variantProductId"), "1")
expect(ProductVariantServiceMock.retrieve).toBeCalledTimes(1)
expect(ProductVariantServiceMock.retrieve).toBeCalledWith("1")
expect(ProductModelMock.findOne).toBeCalledTimes(1)
expect(ProductModelMock.findOne).toBeCalledWith({
_id: IdMap.getId("variantProductId"),
})
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: IdMap.getId("variantProductId") },
{ $push: { variants: "1" } }
)
})
it("throws error if option id is not present in product", async () => {
try {
await productService.addVariant(
IdMap.getId("variantProductId"),
"invalid_option"
)
} catch (err) {
expect(err.message).toEqual(
"Variant options do not contain value for Color"
)
}
})
it("throws error if product variant options is empty", async () => {
try {
await productService.addVariant(
IdMap.getId("variantProductId"),
"empty_option"
)
} catch (err) {
expect(err.message).toEqual(
"Product options length does not match variant options length. Product has 2 and variant has 0."
)
}
})
it("throws error if product options is empty and product variant contains options", async () => {
try {
await productService.addVariant(
IdMap.getId("emptyVariantProductId"),
"1"
)
} catch (err) {
expect(err.message).toEqual(
"Product options length does not match variant options length. Product has 0 and variant has 2."
)
}
})
it("throws error if option values of added variant already exists", async () => {
try {
await productService.addVariant(IdMap.getId("productWithVariants"), "3")
} catch (err) {
expect(err.message).toEqual(
"Variant with provided options already exists"
)
}
})
})
describe("addOption", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("creates variant option values and adds option", async () => {
await productService.addOption(
IdMap.getId("productWithVariants"),
"Material"
)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(3)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"1",
expect.anything(),
"Default Value"
)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"3",
expect.anything(),
"Default Value"
)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"4",
expect.anything(),
"Default Value"
)
expect(ProductModelMock.updateOne).toHaveBeenCalledWith(
{ _id: IdMap.getId("productWithVariants") },
{
$push: {
options: {
_id: expect.anything(),
title: "Material",
product_id: IdMap.getId("productWithVariants"),
},
},
}
)
})
it("cleans up after fail", async () => {
try {
await productService.addOption(
IdMap.getId("productWithVariantsFail"),
"Material"
)
} catch (err) {
expect(err)
}
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(3)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"1",
expect.anything(),
"Default Value"
)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"3",
expect.anything(),
"Default Value"
)
expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith(
"4",
expect.anything(),
"Default Value"
)
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledTimes(
3
)
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith(
"1",
expect.anything()
)
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith(
"3",
expect.anything()
)
expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith(
"4",
expect.anything()
)
})
})
describe("deleteOption", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("deletes an option from a product", async () => {
await productService.deleteOption(
IdMap.getId("productWithVariants"),
IdMap.getId("color_id")
)
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: IdMap.getId("productWithVariants") },
{ $pull: { options: { _id: IdMap.getId("color_id") } } }
)
expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledTimes(3)
expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith(
"1",
IdMap.getId("color_id")
)
expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith(
"3",
IdMap.getId("color_id")
)
expect(ProductVariantServiceMock.deleteOptionValue).toBeCalledWith(
"4",
IdMap.getId("color_id")
)
})
it("if option does not exist, do nothing", async () => {
await productService.deleteOption(
IdMap.getId("productWithVariants"),
IdMap.getId("option_id")
)
expect(ProductModelMock.updateOne).not.toBeCalled()
})
it("throw if variant option values are not equal", async () => {
try {
await productService.deleteOption(
IdMap.getId("productWithFourVariants"),
IdMap.getId("size_id")
)
} catch (err) {
expect(err.message).toEqual(
"To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com"
)
}
expect(ProductModelMock.updateOne).not.toBeCalled()
})
})
describe("removeVariant", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("removes variant from product", async () => {
await productService.removeVariant(
IdMap.getId("productWithVariants"),
"1"
)
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{ _id: IdMap.getId("productWithVariants") },
{ $pull: { variants: "1" } }
)
})
})
describe("updateOption", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("updates title", async () => {
await productService.updateOption(
IdMap.getId("productWithVariants"),
IdMap.getId("color_id"),
{
title: "Shoe Color",
}
)
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{
_id: IdMap.getId("productWithVariants"),
"options._id": IdMap.getId("color_id"),
},
{ $set: { "options.$.title": "Shoe Color" } }
)
})
it("throws if option title exists", async () => {
try {
await productService.updateOption(
IdMap.getId("productWithVariants"),
IdMap.getId("color_id"),
{
title: "Size",
}
)
} catch (err) {
expect(err.message).toEqual("An option with title Size already exists")
}
})
it("throws if option doesn't exist", async () => {
try {
await productService.updateOption(
IdMap.getId("productWithVariants"),
IdMap.getId("material"),
{
title: "Size",
}
)
} catch (err) {
expect(err.message).toEqual(
`Product has no option with id: ${IdMap.getId("material")}`
)
}
})
})
describe("reorderOptions", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("reorders options", async () => {
await productService.reorderOptions(IdMap.getId("productWithVariants"), [
IdMap.getId("size_id"),
IdMap.getId("color_id"),
])
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{
_id: IdMap.getId("productWithVariants"),
},
{
$set: {
options: [
{
_id: IdMap.getId("size_id"),
title: "Size",
},
{
_id: IdMap.getId("color_id"),
title: "Color",
},
],
},
}
)
})
it("throws if one option id is not in the product options", async () => {
try {
await productService.reorderOptions(
IdMap.getId("productWithVariants"),
[IdMap.getId("size_id"), IdMap.getId("material")]
)
} catch (err) {
expect(err.message).toEqual(
`Product has no option with id: ${IdMap.getId("material")}`
)
}
})
it("throws if order length and product option lengths differ", async () => {
try {
await productService.reorderOptions(
IdMap.getId("productWithVariants"),
[
IdMap.getId("size_id"),
IdMap.getId("color_id"),
IdMap.getId("material"),
]
)
} catch (err) {
expect(err.message).toEqual(
`Product options and new options order differ in length. To delete or add options use removeOption or addOption`
)
}
})
})
describe("reorderVariants", () => {
const productService = new ProductService({
productModel: ProductModelMock,
productVariantService: ProductVariantServiceMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it("reorders variants", async () => {
await productService.reorderVariants(IdMap.getId("productWithVariants"), [
"3",
"4",
"1",
])
expect(ProductModelMock.updateOne).toBeCalledTimes(1)
expect(ProductModelMock.updateOne).toBeCalledWith(
{
_id: IdMap.getId("productWithVariants"),
},
{
$set: {
variants: ["3", "4", "1"],
},
}
)
})
it("throws if a variant id is not in the products variants", async () => {
try {
await productService.reorderVariants(
IdMap.getId("productWithVariants"),
["1", "2", "3"]
)
} catch (err) {
expect(err.message).toEqual(`Product has no variant with id: 2`)
}
})
it("throws if order length and product variant lengths differ", async () => {
try {
await productService.reorderVariants(
IdMap.getId("productWithVariants"),
["1", "2", "3", "4"]
)
} catch (err) {
expect(err.message).toEqual(
`Product variants and new variant order differ in length. To delete or add variants use removeVariant or addVariant`
)
}
})
})
})

View File

@@ -0,0 +1,42 @@
import bcrypt from "bcrypt"
import { BaseService } from "../interfaces"
/**
* Can authenticate a user based on email password combination
* @implements BaseService
*/
class AuthService extends BaseService {
/** @param { userModel: (UserModel) } */
constructor({ userModel }) {
super()
/** @private @const {UserModel} */
this.userModel_ = userModel
}
/**
* Authenticates a given user based on an email, password combination. Uses
* bcrypt to match password with hashed value.
* @param {string} email - the email of the user
* @param {string} password - the password of the user
* @return {{ success: (bool), user: (object | undefined) }}
* success: whether authentication succeeded
* user: the user document if authentication succeded
*/
async authenticate(email, password) {
const user = await this.userModel_.findOne({ email })
const passwordsMatch = await bcrypt.compare(password, user.passwordHash)
if (passwordsMatch) {
return {
success: true,
user,
}
} else {
return {
success: false,
}
}
}
}
export default AuthService

View File

@@ -0,0 +1,81 @@
import Bull from "bull"
import config from "../config"
/**
* Can keep track of multiple subscribers to different events and run the
* subscribers when events happen. Events will run asynchronously.
* @interface
*/
class EventBusService {
constructor({ logger }) {
/** @private {logger} */
this.logger_ = logger
/** @private {object} */
this.observers_ = {}
/** @private {BullQueue} */
this.queue_ = new Bull(`${this.constructor.name}:queue`, config.redisURI)
// Register our worker to handle emit calls
this.queue_.process(this.worker_)
}
/**
* Adds a function to a list of event subscribers.
* @param {string} event - the event that the subscriber will listen for.
* @param {func} subscriber - the function to be called when a certain event
* happens. Subscribers must return a Promise.
*/
subscribe(event, subscriber) {
if (typeof subscriber !== "function") {
throw new Error("Subscriber must be a function")
}
if (this.observers_[event]) {
this.observers_[event].push(subscriber)
} else {
this.observers_[event] = [subscriber]
}
}
/**
* Calls all subscribers when an event occurs.
* @param {string} eventName - the name of the event to be process.
* @param {?any} data - the data to send to the subscriber.
* @return {BullJob} - the job from our queue
*/
emit(eventName, data) {
return this.queue_.add({
eventName,
data,
})
}
/**
* Handles incoming jobs.
* @param job {{ eventName: (string), data: (any) }}
* eventName - the name of the event to process
* data - data to send to the subscriber
*
* @returns {Promise} resolves to the results of the subscriber calls.
*/
async worker_({ eventName, data }) {
const observers = this.observers_[eventName] || []
this.logger_.info(
`Processing ${eventName} which has ${observers.length} subscribers`
)
return Promise.all(
observers.map(subscriber => {
return subscriber(data).catch(err => {
this.logger_.warn(
`An error occured while processing ${eventName}: ${err}`
)
return err
})
})
)
}
}
export default EventBusService

View File

@@ -0,0 +1,50 @@
import { BaseService } from "../interfaces"
/**
* Provides layer to manipulate products.
* @implements BaseService
*/
class ProductVariantService extends BaseService {
/** @param { productModel: (ProductModel) } */
constructor({ productVariantModel, eventBusService }) {
super()
/** @private @const {ProductVariantModel} */
this.productVariantModel_ = productVariantModel
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
}
/**
* Creates an unpublished product.
* @param {object} product - the product to create
* @return {Promise} resolves to the creation result.
*/
createDraft(productVariant) {
return this.productVariantModel_.create({
...productVariant,
published: false,
})
}
/**
* Creates an publishes product.
* @param {string} productId - ID of the product to publish.
* @return {Promise} resolves to the creation result.
*/
publish(variantId) {
return this.productVariantModel_.updateOne(
{ _id: variantId },
{ $set: { published: true } }
)
}
/**
*
*/
addOptionValue(variantId, optionId, optionValue) {
}
}
export default ProductVariantService

View File

@@ -0,0 +1,572 @@
import mongoose from "mongoose"
import _ from "lodash"
import { BaseService } from "../interfaces"
import MedusaError, { MedusaErrorTypes } from "../utils/errors"
import validator from "../utils/validator"
/**
* Provides layer to manipulate products.
* @implements BaseService
*/
class ProductService extends BaseService {
/** @param { productModel: (ProductModel) } */
constructor({ productModel, eventBusService, productVariantService }) {
super()
/** @private @const {ProductModel} */
this.productModel_ = productModel
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
/** @private @const {ProductVariantService} */
this.productVariantService_ = productVariantService
}
/**
* Used to validate product ids. Throws an error if the cast fails
* @param {string} rawId - the raw product id to validate.
* @return {string} the validated id
*/
validateId_(rawId) {
const schema = validator.objectId()
const { value, error } = schema.validate(rawId)
if (error) {
throw new MedusaError(
MedusaErrorTypes.INVALID_ARGUMENT,
"The productId could not be casted to an ObjectId"
)
}
return value
}
/**
*
*/
list(query) {
const selector = {}
return this.productModel_.find(selector)
}
/**
* Gets a product by id.
* @param {string} productId - the id of the product to get.
* @return {Promise<Product>} the product document.
*/
retrieve(productId) {
const validatedId = this.validateId_(productId)
return this.productModel_.findOne({ _id: validatedId }).catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
/**
* Creates an unpublished product.
* @param {object} product - the product to create
* @return {Promise} resolves to the creation result.
*/
createDraft(product) {
return this.productModel_
.create({
...product,
published: false,
})
.catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
/**
* Creates an publishes product.
* @param {string} productId - ID of the product to publish.
* @return {Promise} resolves to the creation result.
*/
publish(productId) {
return this.productModel_
.updateOne({ _id: productId }, { $set: { published: true } })
.catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
/**
* Updates a product. Metadata updates and product variant updates should
* use dedicated methods, e.g. `setMetadata`, `addVariant`, etc. The function
* will throw errors if metadata or product variant updates are attempted.
* @param {string} productId - the id of the product. Must be a string that
* can be casted to an ObjectId
* @param {object} update - an object with the update values.
* @return {Promise} resolves to the update result.
*/
update(productId, update) {
const validatedId = this.validateId_(productId)
if (update.metadata) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
"Use setMetadata to update metadata fields"
)
}
if (update.variants) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
"Use addVariant, reorderVariants, removeVariant to update Product Variants"
)
}
return this.productModel_
.updateOne(
{ _id: validatedId },
{ $set: update },
{ runValidators: true }
)
.catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
/**
* Deletes a product from a given product id. The product's associated
* variants will also be deleted.
* @param {string} productId - the id of the product to delete. Must be
* castable as an ObjectId
* @return {Promise} the result of the delete operation.
*/
async delete(productId) {
const product = await this.retrieve(productId)
// Delete is idempotent, but we return a promise to allow then-chaining
if (!product) {
return Promise.resolve()
}
await Promise.all(
product.variants.map(id => this.productVariantService_.delete(id))
).catch(err => {
throw err
})
return this.productModel_.deleteOne({ _id: product._id }).catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
/**
* Adds a product variant to a product. Will check that the given product
* variant has correct option values.
* @param {string} productId - the product the variant will be added to
* @param {string} variantId - the variant to add to the product
* @return {Promise} the result of update
*/
async addVariant(productId, variantId) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
const variant = await this.productVariantService_.retrieve(variantId)
if (!variant) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Variant with ${variantId} was not found`
)
}
if (product.options.length !== variant.options.length) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Product options length does not match variant options length. Product has ${product.options.length} and variant has ${variant.options.length}.`
)
}
product.options.forEach(option => {
if (!variant.options.find(vo => vo.option_id === option._id)) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Variant options do not contain value for ${option.title}`
)
}
})
let combinationExists = false
if (product.variants) {
// Check if option value of the variant to add already exists. Go through
// each existing variant. Check if this variants option values are
// identical to the option values of the variant being added.
combinationExists = product.variants.some(async vId => {
const v = await this.productVariantService_.retrieve(vId)
return v.options.reduce((acc, option, index) => {
return acc && option.value === variant.options[index].value
}, true)
})
}
if (combinationExists) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Variant with provided options already exists`
)
}
return this.productModel_.updateOne(
{ _id: product._id },
{ $push: { variants: variantId } }
)
}
/**
* Adds an option to a product. Options can, for example, be "Size", "Color",
* etc. Will update all the products variants with a dummy value for the newly
* created option. The same option cannot be added more than once.
* @param {string} productId - the product to apply the new option to
* @param {string} optionTitle - the display title of the option, e.g. "Size"
* @return {Promise} the result of the model update operation
*/
async addOption(productId, optionTitle) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
// Make sure that option doesn't already exist
if (product.options.find(o => o.title === optionTitle)) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`An option with the title: ${optionTitle} already exists`
)
}
const optionId = mongoose.Types.ObjectId()
// All product variants must have at least a dummy value for the new option
if (product.variants) {
await Promise.all(
product.variants.map(async variantId =>
this.productVariantService_.addOptionValue(
variantId,
optionId,
"Default Value"
)
)
).catch(err => {
// If any of the variants failed to add the new option value we clean up
return Promise.all(
product.variants.map(async variantId =>
this.productVariantService_.deleteOptionValue(variantId, optionId)
)
).then(() => {
throw err
})
})
}
// Everything went well add the product option
return this.productModel_
.updateOne(
{ _id: productId },
{
$push: {
options: {
_id: optionId,
title: optionTitle,
product_id: productId,
},
},
}
)
.catch(err => {
// If we failed to update the product clean up its variants
return Promise.all(
product.variants.map(async variantId =>
this.productVariantService_.deleteOptionValue(variantId, optionId)
)
).then(() => {
throw err
})
})
}
async reorderVariants(productId, variantOrder) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
if (product.variants.length !== variantOrder.length) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Product variants and new variant order differ in length. To delete or add variants use removeVariant or addVariant`
)
}
const newOrder = variantOrder.map(vId => {
const variant = product.variants.find(id => id === vId)
if (!variant) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Product has no variant with id: ${vId}`
)
}
return variant
})
return this.productModel_.updateOne(
{
_id: productId,
},
{
$set: { variants: newOrder },
}
)
}
/**
* Changes the order of a product's options. Will throw if the length of
* optionOrder and the length of the product's options are different. Will
* throw optionOrder contains an id not associated with the product.
* @param {string} productId - the product whose options we are reordering
* @param {[ObjectId]} optionId - the ids of the product's options in the
* new order
* @return {Promise} the result of the update operation
*/
async reorderOptions(productId, optionOrder) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
if (product.options.length !== optionOrder.length) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Product options and new options order differ in length. To delete or add options use removeOption or addOption`
)
}
const newOrder = optionOrder.map(oId => {
const option = product.options.find(o => o._id === oId)
if (!option) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`Product has no option with id: ${oId}`
)
}
return option
})
return this.productModel_.updateOne(
{
_id: productId,
},
{
$set: { options: newOrder },
}
)
}
/**
* Updates a product's option. Throws if the call tries to update an option
* not associated with the product. Throws if the updated title already exists.
* @param {string} productId - the product whose option we are updating
* @param {string} optionId - the id of the option we are updating
* @param {object} data - the data to update the option with
* @return {Promise} the result of the update operation
*/
async updateOption(productId, optionId, data) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
const option = product.options.find(o => o._id === optionId)
if (!option) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product has no option with id: ${optionId}`
)
}
const { title } = data
const titleExists = product.options.some(
o => o.title.toUpperCase() === title.toUpperCase()
)
if (titleExists) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`An option with title ${title} already exists`
)
}
const update = {}
update["options.$.title"] = title
return this.productModel_.updateOne(
{
_id: productId,
"options._id": optionId,
},
{
$set: update,
}
)
}
/**
* Delete an option from a product.
* @param {string} productId - the product to delete an option from
* @param {string} optionId - the option to delete
* @return {Promise} return the result of update
*/
async deleteOption(productId, optionId) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
if (!product.options.find(o => o._id === optionId)) {
return Promise.resolve()
}
if (product.variants) {
// For the option we want to delete, make sure that all variants have the
// same option values. The reason for doing is, that we want to avoid
// duplicate variants. For example, if we have a product with size and
// color options, that has four variants: (black, 1), (black, 2),
// (blue, 1), (blue, 2) and we delete the size option from the product,
// we would end up with four variants: (black), (black), (blue), (blue).
// We now have two duplicate variants. To ensure that this does not
// happen, we will force the user to select which variants to keep.
const firstVariant = await this.productVariantService_.retrieve(
product.variants[0]
)
const valueToMatch = firstVariant.options.find(
o => o.option_id === optionId
).value
const equalsFirst = await Promise.all(
product.variants.map(async vId => {
const v = await this.productVariantService_.retrieve(vId)
const option = v.options.find(o => o.option_id === optionId)
return option.value === valueToMatch
})
)
if (!equalsFirst.every(v => v)) {
throw new MedusaError(
MedusaErrorTypes.INVALID_DATA,
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com`
)
}
await Promise.all(
product.variants.map(async variantId =>
this.productVariantService_.deleteOptionValue(variantId, optionId)
)
)
}
return this.productModel_.updateOne(
{ _id: productId },
{
$pull: {
options: {
_id: optionId,
},
},
}
)
}
/**
* Removes variant from product
* @param {string} productId - the product to remove the variant from
* @param {string} variantId - the variant to remove from product
* @return {Promise} the result of update
*/
async removeVariant(productId, variantId) {
const product = await this.retrieve(productId)
if (!product) {
throw new MedusaError(
MedusaErrorTypes.NOT_FOUND,
`Product with ${product._id} was not found`
)
}
return this.productModel_.updateOne(
{ _id: productId },
{
$pull: {
variants: variantId,
},
}
)
}
/**
* Decorates a product with product variants.
* @param {Product} product - the product to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {Product} return the decorated product.
*/
async decorate(product, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(product, fields.concat(requiredFields))
if (expandFields.includes("variants")) {
decorated.variants = await Promise.all(
product.variants.map(variantId =>
this.productVariantService_.retrieve(variantId)
)
)
}
return decorated
}
/**
* Sets metadata for a product
* @param {string} productId - the product to decorate.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
setMetadata(productId, key, value) {
const validatedId = this.validateId_(productId)
if (typeof key !== "string") {
throw new MedusaError(
MedusaErrorTypes.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.productModel_
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
.catch(err => {
throw new MedusaError(MedusaErrorTypes.DB_ERROR, err.message)
})
}
}
export default ProductService

View File

@@ -0,0 +1,403 @@
export const countries = [
{ alpha2: "AF", name: "Afghanistan", alpha3: "AFG", numeric: "004" },
{ alpha2: "AL", name: "Albania", alpha3: "ALB", numeric: "008" },
{ alpha2: "DZ", name: "Algeria", alpha3: "DZA", numeric: "012" },
{ alpha2: "AS", name: "American Samoa", alpha3: "ASM", numeric: "016" },
{ alpha2: "AD", name: "Andorra", alpha3: "AND", numeric: "020" },
{ alpha2: "AO", name: "Angola", alpha3: "AGO", numeric: "024" },
{ alpha2: "AI", name: "Anguilla", alpha3: "AIA", numeric: "660" },
{ alpha2: "AQ", name: "Antarctica", alpha3: "ATA", numeric: "010" },
{ alpha2: "AG", name: "Antigua and Barbuda", alpha3: "ATG", numeric: "028" },
{ alpha2: "AR", name: "Argentina", alpha3: "ARG", numeric: "032" },
{ alpha2: "AM", name: "Armenia", alpha3: "ARM", numeric: "051" },
{ alpha2: "AW", name: "Aruba", alpha3: "ABW", numeric: "533" },
{ alpha2: "AU", name: "Australia", alpha3: "AUS", numeric: "036" },
{ alpha2: "AT", name: "Austria", alpha3: "AUT", numeric: "040" },
{ alpha2: "AZ", name: "Azerbaijan", alpha3: "AZE", numeric: "031" },
{ alpha2: "BS", name: "Bahamas", alpha3: "BHS", numeric: "044" },
{ alpha2: "BH", name: "Bahrain", alpha3: "BHR", numeric: "048" },
{ alpha2: "BD", name: "Bangladesh", alpha3: "BGD", numeric: "050" },
{ alpha2: "BB", name: "Barbados", alpha3: "BRB", numeric: "052" },
{ alpha2: "BY", name: "Belarus", alpha3: "BLR", numeric: "112" },
{ alpha2: "BE", name: "Belgium", alpha3: "BEL", numeric: "056" },
{ alpha2: "BZ", name: "Belize", alpha3: "BLZ", numeric: "084" },
{ alpha2: "BJ", name: "Benin", alpha3: "BEN", numeric: "204" },
{ alpha2: "BM", name: "Bermuda", alpha3: "BMU", numeric: "060" },
{ alpha2: "BT", name: "Bhutan", alpha3: "BTN", numeric: "064" },
{ alpha2: "BO", name: "Bolivia", alpha3: "BOL", numeric: "068" },
{
alpha2: "BQ",
name: "Bonaire, Sint Eustatius and Saba",
alpha3: "BES",
numeric: "535",
},
{
alpha2: "BA",
name: "Bosnia and Herzegovina",
alpha3: "BIH",
numeric: "070",
},
{ alpha2: "BW", name: "Botswana", alpha3: "BWA", numeric: "072" },
{ alpha2: "BV", name: "Bouvet Island", alpha3: "BVD", numeric: "074" },
{ alpha2: "BR", name: "Brazil", alpha3: "BRA", numeric: "076" },
{
alpha2: "IO",
name: "British Indian Ocean Territory",
alpha3: "IOT",
numeric: "086",
},
{ alpha2: "BN", name: "Brunei Darussalam", alpha3: "BRN", numeric: "096" },
{ alpha2: "BG", name: "Bulgaria", alpha3: "BGR", numeric: "100" },
{ alpha2: "BF", name: "Burkina Faso", alpha3: "BFA", numeric: "854" },
{ alpha2: "BI", name: "Burundi", alpha3: "BDI", numeric: "108" },
{ alpha2: "KH", name: "Cambodia", alpha3: "KHM", numeric: "116" },
{ alpha2: "CM", name: "Cameroon", alpha3: "CMR", numeric: "120" },
{ alpha2: "CA", name: "Canada", alpha3: "CAN", numeric: "124" },
{ alpha2: "CV", name: "Cape Verde", alpha3: "CPV", numeric: "132" },
{ alpha2: "KY", name: "Cayman Islands", alpha3: "CYM", numeric: "136" },
{
alpha2: "CF",
name: "Central African Republic",
alpha3: "CAF",
numeric: "140",
},
{ alpha2: "TD", name: "Chad", alpha3: "TCD", numeric: "148" },
{ alpha2: "CL", name: "Chile", alpha3: "CHL", numeric: "152" },
{ alpha2: "CN", name: "China", alpha3: "CHN", numeric: "156" },
{ alpha2: "CX", name: "Christmas Island", alpha3: "CXR", numeric: "162" },
{
alpha2: "CC",
name: "Cocos (Keeling) Islands",
alpha3: "CCK",
numeric: "166",
},
{ alpha2: "CO", name: "Colombia", alpha3: "COL", numeric: "170" },
{ alpha2: "KM", name: "Comoros", alpha3: "COM", numeric: "174" },
{ alpha2: "CG", name: "Congo", alpha3: "COG", numeric: "178" },
{
alpha2: "CD",
name: "Congo, the Democratic Republic of the",
alpha3: "COD",
numeric: "180",
},
{ alpha2: "CK", name: "Cook Islands", alpha3: "COK", numeric: "184" },
{ alpha2: "CR", name: "Costa Rica", alpha3: "CRI", numeric: "188" },
{ alpha2: "CI", name: "Cote D'Ivoire", alpha3: "CIV", numeric: "384" },
{ alpha2: "HR", name: "Croatia", alpha3: "HRV", numeric: "191" },
{ alpha2: "CU", name: "Cuba", alpha3: "CUB", numeric: "192" },
{ alpha2: "CW", name: "Curaçao", alpha3: "CUW", numeric: "531" },
{ alpha2: "CY", name: "Cyprus", alpha3: "CYP", numeric: "196" },
{ alpha2: "CZ", name: "Czech Republic", alpha3: "CZE", numeric: "203" },
{ alpha2: "DK", name: "Denmark", alpha3: "DNK", numeric: "208" },
{ alpha2: "DJ", name: "Djibouti", alpha3: "DJI", numeric: "262" },
{ alpha2: "DM", name: "Dominica", alpha3: "DMA", numeric: "212" },
{ alpha2: "DO", name: "Dominican Republic", alpha3: "DOM", numeric: "214" },
{ alpha2: "EC", name: "Ecuador", alpha3: "ECU", numeric: "218" },
{ alpha2: "EG", name: "Egypt", alpha3: "EGY", numeric: "818" },
{ alpha2: "SV", name: "El Salvador", alpha3: "SLV", numeric: "222" },
{ alpha2: "GQ", name: "Equatorial Guinea", alpha3: "GNQ", numeric: "226" },
{ alpha2: "ER", name: "Eritrea", alpha3: "ERI", numeric: "232" },
{ alpha2: "EE", name: "Estonia", alpha3: "EST", numeric: "233" },
{ alpha2: "ET", name: "Ethiopia", alpha3: "ETH", numeric: "231" },
{
alpha2: "FK",
name: "Falkland Islands (Malvinas)",
alpha3: "FLK",
numeric: "238",
},
{ alpha2: "FO", name: "Faroe Islands", alpha3: "FRO", numeric: "234" },
{ alpha2: "FJ", name: "Fiji", alpha3: "FJI", numeric: "242" },
{ alpha2: "FI", name: "Finland", alpha3: "FIN", numeric: "246" },
{ alpha2: "FR", name: "France", alpha3: "FRA", numeric: "250" },
{ alpha2: "GF", name: "French Guiana", alpha3: "GUF", numeric: "254" },
{ alpha2: "PF", name: "French Polynesia", alpha3: "PYF", numeric: "258" },
{
alpha2: "TF",
name: "French Southern Territories",
alpha3: "ATF",
numeric: "260",
},
{ alpha2: "GA", name: "Gabon", alpha3: "GAB", numeric: "266" },
{ alpha2: "GM", name: "Gambia", alpha3: "GMB", numeric: "270" },
{ alpha2: "GE", name: "Georgia", alpha3: "GEO", numeric: "268" },
{ alpha2: "DE", name: "Germany", alpha3: "DEU", numeric: "276" },
{ alpha2: "GH", name: "Ghana", alpha3: "GHA", numeric: "288" },
{ alpha2: "GI", name: "Gibraltar", alpha3: "GIB", numeric: "292" },
{ alpha2: "GR", name: "Greece", alpha3: "GRC", numeric: "300" },
{ alpha2: "GL", name: "Greenland", alpha3: "GRL", numeric: "304" },
{ alpha2: "GD", name: "Grenada", alpha3: "GRD", numeric: "308" },
{ alpha2: "GP", name: "Guadeloupe", alpha3: "GLP", numeric: "312" },
{ alpha2: "GU", name: "Guam", alpha3: "GUM", numeric: "316" },
{ alpha2: "GT", name: "Guatemala", alpha3: "GTM", numeric: "320" },
{ alpha2: "GG", name: "Guernsey", alpha3: "GGY", numeric: "831" },
{ alpha2: "GN", name: "Guinea", alpha3: "GIN", numeric: "324" },
{ alpha2: "GW", name: "Guinea-Bissau", alpha3: "GNB", numeric: "624" },
{ alpha2: "GY", name: "Guyana", alpha3: "GUY", numeric: "328" },
{ alpha2: "HT", name: "Haiti", alpha3: "HTI", numeric: "332" },
{
alpha2: "HM",
name: "Heard Island And Mcdonald Islands",
alpha3: "HMD",
numeric: "334",
},
{
alpha2: "VA",
name: "Holy See (Vatican City State)",
alpha3: "VAT",
numeric: "336",
},
{ alpha2: "HN", name: "Honduras", alpha3: "HND", numeric: "340" },
{ alpha2: "HK", name: "Hong Kong", alpha3: "HKG", numeric: "344" },
{ alpha2: "HU", name: "Hungary", alpha3: "HUN", numeric: "348" },
{ alpha2: "IS", name: "Iceland", alpha3: "ISL", numeric: "352" },
{ alpha2: "IN", name: "India", alpha3: "IND", numeric: "356" },
{ alpha2: "ID", name: "Indonesia", alpha3: "IDN", numeric: "360" },
{
alpha2: "IR",
name: "Iran, Islamic Republic of",
alpha3: "IRN",
numeric: "364",
},
{ alpha2: "IQ", name: "Iraq", alpha3: "IRQ", numeric: "368" },
{ alpha2: "IE", name: "Ireland", alpha3: "IRL", numeric: "372" },
{ alpha2: "IM", name: "Isle Of Man", alpha3: "IMN", numeric: "833" },
{ alpha2: "IL", name: "Israel", alpha3: "ISR", numeric: "376" },
{ alpha2: "IT", name: "Italy", alpha3: "ITA", numeric: "380" },
{ alpha2: "JM", name: "Jamaica", alpha3: "JAM", numeric: "388" },
{ alpha2: "JP", name: "Japan", alpha3: "JPN", numeric: "392" },
{ alpha2: "JE", name: "Jersey", alpha3: "JEY", numeric: "832" },
{ alpha2: "JO", name: "Jordan", alpha3: "JOR", numeric: "400" },
{ alpha2: "KZ", name: "Kazakhstan", alpha3: "KAZ", numeric: "398" },
{ alpha2: "KE", name: "Kenya", alpha3: "KEN", numeric: "404" },
{ alpha2: "KI", name: "Kiribati", alpha3: "KIR", numeric: "296" },
{
alpha2: "KP",
name: "Korea, Democratic People's Republic of",
alpha3: "PRK",
numeric: "408",
},
{ alpha2: "KR", name: "Korea, Republic of", alpha3: "KOR", numeric: "410" },
{ alpha2: "XK", name: "Kosovo", alpha3: "XKX", numeric: "900" },
{ alpha2: "KW", name: "Kuwait", alpha3: "KWT", numeric: "414" },
{ alpha2: "KG", name: "Kyrgyzstan", alpha3: "KGZ", numeric: "417" },
{
alpha2: "LA",
name: "Lao People's Democratic Republic",
alpha3: "LAO",
numeric: "418",
},
{ alpha2: "LV", name: "Latvia", alpha3: "LVA", numeric: "428" },
{ alpha2: "LB", name: "Lebanon", alpha3: "LBN", numeric: "422" },
{ alpha2: "LS", name: "Lesotho", alpha3: "LSO", numeric: "426" },
{ alpha2: "LR", name: "Liberia", alpha3: "LBR", numeric: "430" },
{
alpha2: "LY",
name: "Libyan Arab Jamahiriya",
alpha3: "LBY",
numeric: "434",
},
{ alpha2: "LI", name: "Liechtenstein", alpha3: "LIE", numeric: "438" },
{ alpha2: "LT", name: "Lithuania", alpha3: "LTU", numeric: "440" },
{ alpha2: "LU", name: "Luxembourg", alpha3: "LUX", numeric: "442" },
{ alpha2: "MO", name: "Macao", alpha3: "MAC", numeric: "446" },
{
alpha2: "MK",
name: "Macedonia, the Former Yugoslav Republic of",
alpha3: "MKD",
numeric: "807",
},
{ alpha2: "MG", name: "Madagascar", alpha3: "MDG", numeric: "450" },
{ alpha2: "MW", name: "Malawi", alpha3: "MWI", numeric: "454" },
{ alpha2: "MY", name: "Malaysia", alpha3: "MYS", numeric: "458" },
{ alpha2: "MV", name: "Maldives", alpha3: "MDV", numeric: "462" },
{ alpha2: "ML", name: "Mali", alpha3: "MLI", numeric: "466" },
{ alpha2: "MT", name: "Malta", alpha3: "MLT", numeric: "470" },
{ alpha2: "MH", name: "Marshall Islands", alpha3: "MHL", numeric: "584" },
{ alpha2: "MQ", name: "Martinique", alpha3: "MTQ", numeric: "474" },
{ alpha2: "MR", name: "Mauritania", alpha3: "MRT", numeric: "478" },
{ alpha2: "MU", name: "Mauritius", alpha3: "MUS", numeric: "480" },
{ alpha2: "YT", name: "Mayotte", alpha3: "MYT", numeric: "175" },
{ alpha2: "MX", name: "Mexico", alpha3: "MEX", numeric: "484" },
{
alpha2: "FM",
name: "Micronesia, Federated States of",
alpha3: "FSM",
numeric: "583",
},
{ alpha2: "MD", name: "Moldova, Republic of", alpha3: "MDA", numeric: "498" },
{ alpha2: "MC", name: "Monaco", alpha3: "MCO", numeric: "492" },
{ alpha2: "MN", name: "Mongolia", alpha3: "MNG", numeric: "496" },
{ alpha2: "ME", name: "Montenegro", alpha3: "MNE", numeric: "499" },
{ alpha2: "MS", name: "Montserrat", alpha3: "MSR", numeric: "500" },
{ alpha2: "MA", name: "Morocco", alpha3: "MAR", numeric: "504" },
{ alpha2: "MZ", name: "Mozambique", alpha3: "MOZ", numeric: "508" },
{ alpha2: "MM", name: "Myanmar", alpha3: "MMR", numeric: "104" },
{ alpha2: "NA", name: "Namibia", alpha3: "NAM", numeric: "516" },
{ alpha2: "NR", name: "Nauru", alpha3: "NRU", numeric: "520" },
{ alpha2: "NP", name: "Nepal", alpha3: "NPL", numeric: "524" },
{ alpha2: "NL", name: "Netherlands", alpha3: "NLD", numeric: "528" },
{ alpha2: "NC", name: "New Caledonia", alpha3: "NCL", numeric: "540" },
{ alpha2: "NZ", name: "New Zealand", alpha3: "NZL", numeric: "554" },
{ alpha2: "NI", name: "Nicaragua", alpha3: "NIC", numeric: "558" },
{ alpha2: "NE", name: "Niger", alpha3: "NER", numeric: "562" },
{ alpha2: "NG", name: "Nigeria", alpha3: "NGA", numeric: "566" },
{ alpha2: "NU", name: "Niue", alpha3: "NIU", numeric: "570" },
{ alpha2: "NF", name: "Norfolk Island", alpha3: "NFK", numeric: "574" },
{
alpha2: "MP",
name: "Northern Mariana Islands",
alpha3: "MNP",
numeric: "580",
},
{ alpha2: "NO", name: "Norway", alpha3: "NOR", numeric: "578" },
{ alpha2: "OM", name: "Oman", alpha3: "OMN", numeric: "512" },
{ alpha2: "PK", name: "Pakistan", alpha3: "PAK", numeric: "586" },
{ alpha2: "PW", name: "Palau", alpha3: "PLW", numeric: "585" },
{
alpha2: "PS",
name: "Palestinian Territory, Occupied",
alpha3: "PSE",
numeric: "275",
},
{ alpha2: "PA", name: "Panama", alpha3: "PAN", numeric: "591" },
{ alpha2: "PG", name: "Papua New Guinea", alpha3: "PNG", numeric: "598" },
{ alpha2: "PY", name: "Paraguay", alpha3: "PRY", numeric: "600" },
{ alpha2: "PE", name: "Peru", alpha3: "PER", numeric: "604" },
{ alpha2: "PH", name: "Philippines", alpha3: "PHL", numeric: "608" },
{ alpha2: "PN", name: "Pitcairn", alpha3: "PCN", numeric: "612" },
{ alpha2: "PL", name: "Poland", alpha3: "POL", numeric: "616" },
{ alpha2: "PT", name: "Portugal", alpha3: "PRT", numeric: "620" },
{ alpha2: "PR", name: "Puerto Rico", alpha3: "PRI", numeric: "630" },
{ alpha2: "QA", name: "Qatar", alpha3: "QAT", numeric: "634" },
{ alpha2: "RE", name: "Reunion", alpha3: "REU", numeric: "638" },
{ alpha2: "RO", name: "Romania", alpha3: "ROU", numeric: "642" },
{ alpha2: "RO", name: "Romania", alpha3: "ROM", numeric: "642" },
{ alpha2: "RU", name: "Russian Federation", alpha3: "RUS", numeric: "643" },
{ alpha2: "RW", name: "Rwanda", alpha3: "RWA", numeric: "646" },
{ alpha2: "BL", name: "Saint Barthélemy", alpha3: "BLM", numeric: "652" },
{ alpha2: "SH", name: "Saint Helena", alpha3: "SHN", numeric: "654" },
{
alpha2: "KN",
name: "Saint Kitts and Nevis",
alpha3: "KNA",
numeric: "659",
},
{ alpha2: "LC", name: "Saint Lucia", alpha3: "LCA", numeric: "662" },
{
alpha2: "MF",
name: "Saint Martin (French part)",
alpha3: "MAF",
numeric: "663",
},
{
alpha2: "PM",
name: "Saint Pierre and Miquelon",
alpha3: "SPM",
numeric: "666",
},
{
alpha2: "VC",
name: "Saint Vincent and the Grenadines",
alpha3: "VCT",
numeric: "670",
},
{ alpha2: "WS", name: "Samoa", alpha3: "WSM", numeric: "882" },
{ alpha2: "SM", name: "San Marino", alpha3: "SMR", numeric: "674" },
{
alpha2: "ST",
name: "Sao Tome and Principe",
alpha3: "STP",
numeric: "678",
},
{ alpha2: "SA", name: "Saudi Arabia", alpha3: "SAU", numeric: "682" },
{ alpha2: "SN", name: "Senegal", alpha3: "SEN", numeric: "686" },
{ alpha2: "RS", name: "Serbia", alpha3: "SRB", numeric: "688" },
{ alpha2: "SC", name: "Seychelles", alpha3: "SYC", numeric: "690" },
{ alpha2: "SL", name: "Sierra Leone", alpha3: "SLE", numeric: "694" },
{ alpha2: "SG", name: "Singapore", alpha3: "SGP", numeric: "702" },
{ alpha2: "SX", name: "Sint Maarten", alpha3: "SXM", numeric: "534" },
{ alpha2: "SK", name: "Slovakia", alpha3: "SVK", numeric: "703" },
{ alpha2: "SI", name: "Slovenia", alpha3: "SVN", numeric: "705" },
{ alpha2: "SB", name: "Solomon Islands", alpha3: "SLB", numeric: "090" },
{ alpha2: "SO", name: "Somalia", alpha3: "SOM", numeric: "706" },
{ alpha2: "ZA", name: "South Africa", alpha3: "ZAF", numeric: "710" },
{
alpha2: "GS",
name: "South Georgia and the South Sandwich Islands",
alpha3: "SGS",
numeric: "239",
},
{ alpha2: "SS", name: "South Sudan", alpha3: "SSD", numeric: "728" },
{ alpha2: "ES", name: "Spain", alpha3: "ESP", numeric: "724" },
{ alpha2: "LK", name: "Sri Lanka", alpha3: "LKA", numeric: "144" },
{ alpha2: "SD", name: "Sudan", alpha3: "SDN", numeric: "729" },
{ alpha2: "SR", name: "Suriname", alpha3: "SUR", numeric: "740" },
{
alpha2: "SJ",
name: "Svalbard and Jan Mayen",
alpha3: "SJM",
numeric: "744",
},
{ alpha2: "SZ", name: "Swaziland", alpha3: "SWZ", numeric: "748" },
{ alpha2: "SE", name: "Sweden", alpha3: "SWE", numeric: "752" },
{ alpha2: "CH", name: "Switzerland", alpha3: "CHE", numeric: "756" },
{ alpha2: "SY", name: "Syrian Arab Republic", alpha3: "SYR", numeric: "760" },
{
alpha2: "TW",
name: "Taiwan, Province of China",
alpha3: "TWN",
numeric: "158",
},
{ alpha2: "TJ", name: "Tajikistan", alpha3: "TJK", numeric: "762" },
{
alpha2: "TZ",
name: "Tanzania, United Republic of",
alpha3: "TZA",
numeric: "834",
},
{ alpha2: "TH", name: "Thailand", alpha3: "THA", numeric: "764" },
{ alpha2: "TL", name: "Timor Leste", alpha3: "TLS", numeric: "626" },
{ alpha2: "TG", name: "Togo", alpha3: "TGO", numeric: "768" },
{ alpha2: "TK", name: "Tokelau", alpha3: "TKL", numeric: "772" },
{ alpha2: "TO", name: "Tonga", alpha3: "TON", numeric: "776" },
{ alpha2: "TT", name: "Trinidad and Tobago", alpha3: "TTO", numeric: "780" },
{ alpha2: "TN", name: "Tunisia", alpha3: "TUN", numeric: "788" },
{ alpha2: "TR", name: "Turkey", alpha3: "TUR", numeric: "792" },
{ alpha2: "TM", name: "Turkmenistan", alpha3: "TKM", numeric: "795" },
{
alpha2: "TC",
name: "Turks and Caicos Islands",
alpha3: "TCA",
numeric: "796",
},
{ alpha2: "TV", name: "Tuvalu", alpha3: "TUV", numeric: "798" },
{ alpha2: "UG", name: "Uganda", alpha3: "UGA", numeric: "800" },
{ alpha2: "UA", name: "Ukraine", alpha3: "UKR", numeric: "804" },
{ alpha2: "AE", name: "United Arab Emirates", alpha3: "ARE", numeric: "784" },
{ alpha2: "GB", name: "United Kingdom", alpha3: "GBR", numeric: "826" },
{ alpha2: "US", name: "United States", alpha3: "USA", numeric: "840" },
{
alpha2: "UM",
name: "United States Minor Outlying Islands",
alpha3: "UMI",
numeric: "581",
},
{ alpha2: "UY", name: "Uruguay", alpha3: "URY", numeric: "858" },
{ alpha2: "UZ", name: "Uzbekistan", alpha3: "UZB", numeric: "860" },
{ alpha2: "VU", name: "Vanuatu", alpha3: "VUT", numeric: "548" },
{ alpha2: "VE", name: "Venezuela", alpha3: "VEN", numeric: "862" },
{ alpha2: "VN", name: "Viet Nam", alpha3: "VNM", numeric: "704" },
{
alpha2: "VG",
name: "Virgin Islands, British",
alpha3: "VGB",
numeric: "092",
},
{ alpha2: "VI", name: "Virgin Islands, U.S.", alpha3: "VIR", numeric: "850" },
{ alpha2: "WF", name: "Wallis and Futuna", alpha3: "WLF", numeric: "876" },
{ alpha2: "EH", name: "Western Sahara", alpha3: "ESH", numeric: "732" },
{ alpha2: "YE", name: "Yemen", alpha3: "YEM", numeric: "887" },
{ alpha2: "ZM", name: "Zambia", alpha3: "ZMB", numeric: "894" },
{ alpha2: "ZW", name: "Zimbabwe", alpha3: "ZWE", numeric: "716" },
{ alpha2: "AX", name: "Åland Islands", alpha3: "ALA", numeric: "248" },
]

View File

@@ -0,0 +1,36 @@
/**
* @typedef MedusaErrorType
*
*/
export const MedusaErrorTypes = {
/** Errors stemming from the database */
DB_ERROR: "database_error",
INVALID_ARGUMENT: "invalid_argument",
INVALID_DATA: "invalid_data",
NOT_FOUND: "not_found"
}
/**
* Standardized error to be used across Medusa project.
* @extends Error
*/
class MedusaError extends Error {
/**
* Creates a standardized error to be used across Medusa project.
* @param type {MedusaErrorType} - the type of error.
* @param params {Array} - Error params.
*/
constructor(name, message, ...params) {
super(...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, MedusaError)
}
this.name = name
this.message = message
this.date = new Date()
}
}
export default MedusaError

View File

@@ -0,0 +1,4 @@
import Joi from "@hapi/joi"
Joi.objectId = require("joi-objectid")(Joi)
export default Joi

6324
packages/medusa/yarn.lock Normal file

File diff suppressed because it is too large Load Diff