Merge master

This commit is contained in:
Sebastian Rindom
2020-06-29 16:23:54 +02:00
29 changed files with 1024 additions and 24 deletions
+3 -3
View File
@@ -4,11 +4,11 @@ import store from "./routes/store"
import errorHandler from "./middlewares/error-handler"
// guaranteed to get dependencies
export default container => {
export default (container, config) => {
const app = Router()
admin(app, container)
store(app, container)
admin(app, container, config)
store(app, container, config)
app.use(errorHandler())
+14 -3
View File
@@ -1,4 +1,6 @@
import { Router } from "express"
import cors from "cors"
import middlewares from "../../middlewares"
import authRoutes from "./auth"
import productRoutes from "./products"
@@ -8,17 +10,25 @@ import shippingOptionRoutes from "./shipping-options"
import shippingProfileRoutes from "./shipping-profiles"
import discountRoutes from "./discounts"
import orderRoutes from "./orders"
import storeRoutes from "./store"
const route = Router()
export default (app, container) => {
const middlewareService = container.resolve("middlewareService")
export default (app, container, config) => {
app.use("/admin", route)
const adminCors = config.admin_cors || ""
route.use(
cors({
origin: adminCors.split(","),
credentials: true,
})
)
// Unauthenticated routes
authRoutes(route)
const middlewareService = container.resolve("middlewareService")
// Calls all middleware that has been registered to run before authentication.
middlewareService.usePreAuthentication(app)
@@ -35,6 +45,7 @@ export default (app, container) => {
shippingProfileRoutes(route)
discountRoutes(route)
orderRoutes(route)
storeRoutes(route)
return app
}
@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { StoreServiceMock } from "../../../../../services/__mocks__/store"
describe("POST /admin/store/currencies/:currency_code", () => {
describe("successful addition", () => {
let subject
beforeAll(async () => {
subject = await request("POST", `/admin/store/currencies/dkk`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(StoreServiceMock.addCurrency).toHaveBeenCalledTimes(1)
expect(StoreServiceMock.addCurrency).toHaveBeenCalledWith("dkk")
})
})
})
@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { StoreServiceMock } from "../../../../../services/__mocks__/store"
describe("GET /admin/store", () => {
describe("successful addition", () => {
let subject
beforeAll(async () => {
subject = await request("GET", `/admin/store`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(StoreServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(StoreServiceMock.retrieve).toHaveBeenCalledWith()
})
})
})
@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { StoreServiceMock } from "../../../../../services/__mocks__/store"
describe("DELETE /admin/store/currencies/:currency_code", () => {
describe("successful addition", () => {
let subject
beforeAll(async () => {
subject = await request("DELETE", `/admin/store/currencies/dkk`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(StoreServiceMock.removeCurrency).toHaveBeenCalledTimes(1)
expect(StoreServiceMock.removeCurrency).toHaveBeenCalledWith("dkk")
})
})
})
@@ -0,0 +1,63 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { StoreServiceMock } from "../../../../../services/__mocks__/store"
describe("POST /admin/store", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/store", {
payload: {
name: "New Name",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service update", () => {
expect(StoreServiceMock.update).toHaveBeenCalledTimes(1)
expect(StoreServiceMock.update).toHaveBeenCalledWith({
name: "New Name",
})
})
})
describe("successful creation", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/store", {
payload: {
currencies: ["DKK", "USD"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service update", () => {
expect(StoreServiceMock.update).toHaveBeenCalledTimes(1)
expect(StoreServiceMock.update).toHaveBeenCalledWith({
currencies: ["DKK", "USD"],
})
})
})
})
@@ -0,0 +1,11 @@
export default async (req, res) => {
const { currency_code } = req.params
try {
const storeService = req.scope.resolve("storeService")
const data = await storeService.addCurrency(currency_code)
res.status(200).json({ store: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,9 @@
export default async (req, res) => {
try {
const storeService = req.scope.resolve("storeService")
const data = await storeService.retrieve()
res.status(200).json({ store: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,21 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/store", route)
route.get("/", middlewares.wrap(require("./get-store").default))
route.post("/", middlewares.wrap(require("./update-store").default))
route.post(
"/currencies/:currency_code",
middlewares.wrap(require("./add-currency").default)
)
route.delete(
"/currencies/:currency_code",
middlewares.wrap(require("./remove-currency").default)
)
return app
}
@@ -0,0 +1,11 @@
export default async (req, res) => {
const { currency_code } = req.params
try {
const storeService = req.scope.resolve("storeService")
const data = await storeService.removeCurrency(currency_code)
res.status(200).json({ store: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,21 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
name: Validator.string(),
currencies: Validator.array().items(Validator.string()),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const storeService = req.scope.resolve("storeService")
const data = await storeService.update(value)
res.status(200).json({ store: data })
} catch (err) {
throw err
}
}
+10 -1
View File
@@ -1,4 +1,5 @@
import { Router } from "express"
import cors from "cors"
import productRoutes from "./products"
import cartRoutes from "./carts"
@@ -9,9 +10,17 @@ import regionRoutes from "./regions"
const route = Router()
export default app => {
export default (app, container, config) => {
app.use("/store", route)
const storeCors = config.store_cors || ""
route.use(
cors({
origin: storeCors.split(","),
credentials: true,
})
)
customerRoutes(route)
productRoutes(route)
orderRoutes(route)
+3 -2
View File
@@ -1,9 +1,10 @@
import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
import routes from "../api"
export default async ({ app, rootDirectory, container }) => {
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
const config = (configModule && configModule.projectConfig) || {}
app.use("/", routes(container))
app.use("/", routes(container, config))
return app
}
-3
View File
@@ -2,15 +2,12 @@ import express from "express"
import bodyParser from "body-parser"
import session from "client-sessions"
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", {
skip: () => process.env.NODE_ENV === "test",
+5 -1
View File
@@ -6,6 +6,7 @@ import modelsLoader from "./models"
import servicesLoader from "./services"
import passportLoader from "./passport"
import pluginsLoader from "./plugins"
import storeLoader from "./store"
import Logger from "./logger"
export default async ({ directory: rootDirectory, expressApp }) => {
@@ -54,9 +55,12 @@ export default async ({ directory: rootDirectory, expressApp }) => {
await pluginsLoader({ container, rootDirectory, app: expressApp })
Logger.info("Plugins Intialized")
await apiLoader({ container, app: expressApp })
await apiLoader({ container, rootDirectory, app: expressApp })
Logger.info("API initialized")
await storeLoader({ container })
Logger.info("Store initialized")
return { container, dbConnection, app: expressApp }
}
+10 -7
View File
@@ -9,7 +9,7 @@ import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
import _ from "lodash"
import path from "path"
import fs from "fs"
import { asFunction } from "awilix"
import { asFunction, aliasTo } from "awilix"
import { sync as existsSync } from "fs-exists-cached"
/**
@@ -112,6 +112,7 @@ function registerServices(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
files.forEach(fn => {
const loaded = require(fn).default
const name = formatRegistrationName(fn)
if (!(loaded.prototype instanceof BaseService)) {
const logger = container.resolve("logger")
@@ -130,9 +131,8 @@ function registerServices(pluginDetails, container) {
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[`pp_${loaded.identifier}`]: asFunction(
cradle => new loaded(cradle, pluginDetails.options)
),
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
[`pp_${loaded.identifier}`]: aliasTo(name),
})
} else if (loaded.prototype instanceof FulfillmentService) {
// Register our payment providers to paymentProviders
@@ -144,9 +144,12 @@ function registerServices(pluginDetails, container) {
// Add the service directly to the container in order to make simple
// resolution if we already know which payment provider we need to use
container.register({
[`fp_${loaded.identifier}`]: asFunction(
cradle => new loaded(cradle, pluginDetails.options)
),
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
[`fp_${loaded.identifier}`]: aliasTo(name),
})
} else {
container.register({
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
})
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Registers all services in the services directory
*/
export default ({ container }) => {
const storeService = container.resolve("storeService")
return storeService.create()
}
@@ -0,0 +1,17 @@
import { IdMap } from "medusa-test-utils"
export const store = {
_id: IdMap.getId("store"),
name: "test store",
currencies: ["DKK"],
}
export const StoreModelMock = {
create: jest.fn().mockReturnValue(Promise.resolve()),
updateOne: jest.fn().mockImplementation((query, update) => {
return Promise.resolve()
}),
findOne: jest.fn().mockImplementation(query => {
return Promise.resolve(store)
}),
}
+13
View File
@@ -0,0 +1,13 @@
import mongoose from "mongoose"
import { BaseModel } from "medusa-interfaces"
class StoreModel extends BaseModel {
static modelName = "Store"
static schema = {
name: { type: String, required: true, default: "Medusa Store" },
currencies: { type: [String], default: [] },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
}
}
export default StoreModel
@@ -0,0 +1,28 @@
import { IdMap } from "medusa-test-utils"
export const store = {
_id: IdMap.getId("store"),
name: "Test store",
currencies: ["DKK", "SEK", "GBP"],
}
export const StoreServiceMock = {
addCurrency: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),
removeCurrency: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),
update: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),
retrieve: jest.fn().mockImplementation(data => {
return Promise.resolve(store)
}),
}
const mock = jest.fn().mockImplementation(() => {
return StoreServiceMock
})
export default mock
@@ -4,6 +4,7 @@ import RegionService from "../region"
import { RegionModelMock } from "../../models/__mocks__/region"
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"
import { FulfillmentProviderServiceMock } from "../__mocks__/fulfillment-provider"
import { StoreServiceMock } from "../__mocks__/store"
describe("RegionService", () => {
describe("create", () => {
@@ -14,6 +15,7 @@ describe("RegionService", () => {
it("successfully creates a new region", async () => {
const regionService = new RegionService({
regionModel: RegionModelMock,
storeService: StoreServiceMock,
})
await regionService.create({
@@ -37,6 +39,7 @@ describe("RegionService", () => {
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
storeService: StoreServiceMock,
})
await regionService.create({
@@ -103,6 +106,7 @@ describe("RegionService", () => {
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
storeService: StoreServiceMock,
})
await expect(
@@ -195,6 +199,7 @@ describe("RegionService", () => {
regionModel: RegionModelMock,
paymentProviderService: PaymentProviderServiceMock,
fulfillmentProviderService: FulfillmentProviderServiceMock,
storeService: StoreServiceMock,
})
await regionService.update(IdMap.getId("region-se"), {
@@ -0,0 +1,127 @@
import StoreService from "../store"
import { StoreModelMock } from "../../models/__mocks__/store"
import { IdMap } from "medusa-test-utils"
describe("StoreService", () => {
describe("retrieve", () => {
const storeService = new StoreService({
storeModel: StoreModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("retrieves store", async () => {
await storeService.retrieve()
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.findOne).toHaveBeenCalledWith()
})
})
describe("update", () => {
const storeService = new StoreService({
storeModel: StoreModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("retrieves store", async () => {
await storeService.update({
name: "New Name",
currencies: ["DKK", "sek", "uSd"],
})
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledWith(
{ _id: IdMap.getId("store") },
{
$set: {
name: "New Name",
currencies: ["DKK", "SEK", "USD"],
},
},
{ runValidators: true }
)
})
it("fails if currency not ok", async () => {
await expect(
storeService.update({
currencies: ["notacurrence"],
})
).rejects.toThrow("Invalid currency NOTACURRENCE")
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
})
})
describe("addCurrency", () => {
const storeService = new StoreService({
storeModel: StoreModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("retrieves store", async () => {
await storeService.addCurrency("sek")
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledWith(
{ _id: IdMap.getId("store") },
{
$push: { currencies: "SEK" },
}
)
})
it("fails if currency not ok", async () => {
await expect(storeService.addCurrency("notacurrence")).rejects.toThrow(
"Invalid currency NOTACURRENCE"
)
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
})
it("fails if currency already existis", async () => {
await expect(storeService.addCurrency("DKK")).rejects.toThrow(
"Currency already added"
)
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
})
})
describe("removeCurrency", () => {
const storeService = new StoreService({
storeModel: StoreModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("retrieves store", async () => {
await storeService.removeCurrency("sek")
expect(StoreModelMock.findOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(StoreModelMock.updateOne).toHaveBeenCalledWith(
{ _id: IdMap.getId("store") },
{
$pull: { currencies: "SEK" },
}
)
})
})
})
+9 -4
View File
@@ -2,7 +2,6 @@ import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { countries } from "../utils/countries"
import { currencies } from "../utils/currencies"
/**
* Provides layer to manipulate regions.
@@ -11,6 +10,7 @@ import { currencies } from "../utils/currencies"
class RegionService extends BaseService {
constructor({
regionModel,
storeService,
paymentProviderService,
fulfillmentProviderService,
}) {
@@ -19,6 +19,9 @@ class RegionService extends BaseService {
/** @private @const {RegionModel} */
this.regionModel_ = regionModel
/** @private @const {StoreService} */
this.storeService_ = storeService
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
@@ -69,7 +72,7 @@ class RegionService extends BaseService {
if (region.currency_code) {
region.currency_code = region.currency_code.toUpperCase()
this.validateCurrency_(region.currency_code)
await this.validateCurrency_(region.currency_code)
}
if (region.countries) {
@@ -123,8 +126,10 @@ class RegionService extends BaseService {
* Validates a currency code. Will throw if the currency code doesn't exist.
* @param {string} currencyCode - an ISO currency code
*/
validateCurrency_(currencyCode) {
if (!currencies[currencyCode]) {
async validateCurrency_(currencyCode) {
const store = await this.storeService_.retrieve()
if (!store.currencies.includes(currencyCode.toUpperCase())) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Invalid currency code"
+188
View File
@@ -0,0 +1,188 @@
import mongoose from "mongoose"
import bcrypt from "bcrypt"
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { currencies } from "../utils/currencies"
/**
* Provides layer to manipulate store settings.
* @implements BaseService
*/
class StoreService extends BaseService {
constructor({ storeModel, eventBusService }) {
super()
/** @private @const {storeModel} */
this.storeModel_ = storeModel
/** @private @const {EventBus} */
this.eventBus_ = eventBusService
}
/**
* Used to validate customer ids. Throws an error if the cast fails
* @param {string} rawId - the raw customer 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(
MedusaError.Types.INVALID_ARGUMENT,
"The customerId could not be casted to an ObjectId"
)
}
return value
}
/**
* Creates a store if it doesn't already exist.
* @return {Promise<Store>} the store.
*/
async create() {
const store = await this.retrieve()
if (!store) {
return this.storeModel_.create({})
}
return store
}
/**
* Retrieve the store settings. There is always a maximum of one store.
* @return {Promise<Store>} the customer document.
*/
retrieve() {
return this.storeModel_.findOne().catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Updates a customer. Metadata updates and address updates should
* use dedicated methods, e.g. `setMetadata`, etc. The function
* will throw errors if metadata updates and address updates are attempted.
* @param {string} variantId - the id of the variant. 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.
*/
async update(update) {
const store = await this.retrieve()
if (update.metadata) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Use setMetadata to update metadata fields"
)
}
if (update.currencies) {
update.currencies = update.currencies.map(c => c.toUpperCase())
update.currencies.forEach(c => {
if (!currencies[c]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid currency ${c}`
)
}
})
}
return this.storeModel_
.updateOne({ _id: store._id }, { $set: update }, { runValidators: true })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Add a currency to the store
* @param {string} code - 3 character ISO currency code
* @return {Promise} result after update
*/
async addCurrency(code) {
code = code.toUpperCase()
const store = await this.retrieve()
if (!currencies[code]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid currency ${code}`
)
}
if (store.currencies.includes(code)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Currency already added`
)
}
return this.storeModel_.updateOne(
{
_id: store._id,
},
{ $push: { currencies: code } }
)
}
/**
* Removes a currency from the store
* @param {string} code - 3 character ISO currency code
* @return {Promise} result after update
*/
async removeCurrency(code) {
const store = await this.retrieve()
code = code.toUpperCase()
return this.storeModel_.updateOne(
{
_id: store._id,
},
{ $pull: { currencies: code } }
)
}
/**
* Decorates a store object.
* @param {Store} store - the store to decorate.
* @param {string[]} fields - the fields to include.
* @param {string[]} expandFields - fields to expand.
* @return {Store} return the decorated Store.
*/
async decorate(store, fields, expandFields = []) {
return store
}
/**
* Dedicated method to set metadata for a store.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} customerId - the customer to apply metadata to.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
async setMetadata(key, value) {
const store = await this.retrieve()
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
const keyPath = `metadata.${key}`
return this.storeModel_
.updateOne({ _id: store._id }, { $set: { [keyPath]: value } })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
}
export default StoreService