Adds Middleware API (#40)
* Adds test project * Adds e2e for end-2-end tests * Update gitignore * Update loaders * Creates test project * v0.1.27 * Upgrade * dependency fixes * Load project plugins in the plugin loader * Issue with instanceof * Fixes versioning issues * Adds medusa middleware api * Adds documentation * Fixes tests
This commit is contained in:
1 parent
964e26e310
commit
516bc7675d
46 files changed
+10163
-977
No files matched your search
@@ -7,7 +7,11 @@ if (process.env.NODE_ENV !== `test`) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plugins: ["@babel/plugin-proposal-class-properties"],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-classes",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
],
|
||||
presets: ["@babel/preset-env"],
|
||||
env: {
|
||||
test: {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
@@ -31,12 +32,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon --watch plugins/ --watch src/ --exec babel-node src/app.js",
|
||||
"watch": "babel -w src --out-dir dist --ignore **/__tests__",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"build": "babel src -d dist",
|
||||
"serve": "node dist/app.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"awilix": "^4.2.3",
|
||||
"bcrypt": "^3.0.7",
|
||||
|
||||
@@ -4,11 +4,11 @@ import store from "./routes/store"
|
||||
import errorHandler from "./middlewares/error-handler"
|
||||
|
||||
// guaranteed to get dependencies
|
||||
export default () => {
|
||||
export default container => {
|
||||
const app = Router()
|
||||
|
||||
admin(app)
|
||||
store(app)
|
||||
admin(app, container)
|
||||
store(app, container)
|
||||
|
||||
app.use(errorHandler())
|
||||
|
||||
|
||||
@@ -11,15 +11,23 @@ import discountRoutes from "./discounts"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
export default (app, container) => {
|
||||
const middlewareService = container.resolve("middlewareService")
|
||||
|
||||
app.use("/admin", route)
|
||||
|
||||
// Unauthenticated routes
|
||||
authRoutes(route)
|
||||
|
||||
// Calls all middleware that has been registered to run before authentication.
|
||||
middlewareService.usePreAuthentication(app)
|
||||
|
||||
// Authenticated routes
|
||||
route.use(middlewares.authenticate())
|
||||
|
||||
// Calls all middleware that has been registered to run after authentication.
|
||||
middlewareService.usePostAuthentication(app)
|
||||
|
||||
productRoutes(route)
|
||||
userRoutes(route)
|
||||
regionRoutes(route)
|
||||
|
||||
@@ -29,7 +29,7 @@ testApp.use((req, res, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
apiLoader({ app: testApp })
|
||||
apiLoader({ container, app: testApp })
|
||||
|
||||
const supertestRequest = supertest(testApp)
|
||||
|
||||
|
||||
@@ -3,18 +3,7 @@ import routes from "../api"
|
||||
import glob from "glob"
|
||||
import path from "path"
|
||||
|
||||
export default async ({ app }) => {
|
||||
const corePath = path.join(__dirname, "../api")
|
||||
const appPath = path.resolve("src/api")
|
||||
|
||||
if (corePath !== appPath) {
|
||||
const appRoutes = require(appPath).default
|
||||
if (appRoutes) {
|
||||
app.use("/", appRoutes())
|
||||
}
|
||||
}
|
||||
|
||||
app.use("/", routes())
|
||||
|
||||
export default async ({ app, container }) => {
|
||||
app.use("/", routes(container))
|
||||
return app
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export default async ({ expressApp }) => {
|
||||
await servicesLoader({ container })
|
||||
Logger.info("Services initialized")
|
||||
|
||||
await mongooseLoader()
|
||||
await mongooseLoader({ container })
|
||||
Logger.info("MongoDB Intialized")
|
||||
|
||||
await expressLoader({ app: expressApp })
|
||||
@@ -54,7 +54,7 @@ export default async ({ expressApp }) => {
|
||||
await pluginsLoader({ container, app: expressApp })
|
||||
Logger.info("Plugins Intialized")
|
||||
|
||||
await apiLoader({ app: expressApp })
|
||||
await apiLoader({ container, app: expressApp })
|
||||
Logger.info("API initialized")
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ import { asFunction } from "awilix"
|
||||
*/
|
||||
export default ({ container }) => {
|
||||
let corePath = "../models/*.js"
|
||||
let appPath = "src/models/*.js"
|
||||
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
const appFull = path.resolve(appPath)
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach(fn => {
|
||||
@@ -22,25 +19,6 @@ export default ({ container }) => {
|
||||
[name]: asFunction(cradle => new loaded(cradle)).singleton(),
|
||||
})
|
||||
})
|
||||
|
||||
if (coreFull !== appFull) {
|
||||
const files = glob.sync(appFull)
|
||||
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)).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function formatRegistrationName(fn) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import mongoose from "mongoose"
|
||||
import config from "../config"
|
||||
|
||||
export default async () => {
|
||||
const connection = await mongoose.connect(config.databaseURL, {
|
||||
useNewUrlParser: true,
|
||||
useCreateIndex: true,
|
||||
useUnifiedTopology: true,
|
||||
export default async ({ container }) => {
|
||||
const logger = container.resolve("logger")
|
||||
await mongoose
|
||||
.connect(config.databaseURL, {
|
||||
useNewUrlParser: true,
|
||||
useCreateIndex: true,
|
||||
useUnifiedTopology: true,
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(err)
|
||||
})
|
||||
|
||||
mongoose.connection.on("error", err => {
|
||||
logger.error(err)
|
||||
})
|
||||
return connection.connection.db
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PaymentService,
|
||||
FulfillmentService,
|
||||
} from "medusa-interfaces"
|
||||
import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
|
||||
import _ from "lodash"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
@@ -15,8 +16,16 @@ import { sync as existsSync } from "fs-exists-cached"
|
||||
* Registers all services in the services directory
|
||||
*/
|
||||
export default ({ container, app }) => {
|
||||
const configPath = path.resolve("./medusa-config")
|
||||
const { plugins } = require(configPath)
|
||||
const { configModule, configFilePath } = getConfigFile(
|
||||
process.cwd(),
|
||||
`medusa-config`
|
||||
)
|
||||
|
||||
if (!configModule) {
|
||||
return
|
||||
}
|
||||
|
||||
const { plugins } = configModule
|
||||
|
||||
const resolved = plugins.map(plugin => {
|
||||
if (_.isString(plugin)) {
|
||||
@@ -29,13 +38,53 @@ export default ({ container, app }) => {
|
||||
return details
|
||||
})
|
||||
|
||||
resolved.push({
|
||||
resolve: process.cwd(),
|
||||
name: `project-plugin`,
|
||||
id: createPluginId(`project-plugin`),
|
||||
options: {},
|
||||
version: createFileContentHash(process.cwd(), `**`),
|
||||
})
|
||||
|
||||
resolved.forEach(pluginDetails => {
|
||||
registerServices(pluginDetails, container)
|
||||
registerModels(pluginDetails, container)
|
||||
registerServices(pluginDetails, container)
|
||||
registerMedusaApi(pluginDetails, container)
|
||||
registerApi(pluginDetails, app)
|
||||
})
|
||||
}
|
||||
|
||||
function registerMedusaApi(pluginDetails, container) {
|
||||
registerMedusaMiddleware(pluginDetails, container)
|
||||
}
|
||||
|
||||
function registerMedusaMiddleware(pluginDetails, container) {
|
||||
let module
|
||||
try {
|
||||
module = require(`${pluginDetails.resolve}/api/medusa-middleware`).default
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
|
||||
const middlewareService = container.resolve("middlewareService")
|
||||
if (module.postAuthentication) {
|
||||
middlewareService.addPostAuthentication(
|
||||
module.postAuthentication,
|
||||
pluginDetails.options
|
||||
)
|
||||
}
|
||||
|
||||
if (module.preAuthentication) {
|
||||
middlewareService.addPreAuthentication(
|
||||
module.preAuthentication,
|
||||
pluginDetails.options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the plugin's api routes.
|
||||
*/
|
||||
function registerApi(pluginDetails, app) {
|
||||
try {
|
||||
const routes = require(`${pluginDetails.resolve}/api`).default
|
||||
@@ -59,7 +108,7 @@ function registerApi(pluginDetails, app) {
|
||||
* @return {void}
|
||||
*/
|
||||
function registerServices(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/services/*`, {})
|
||||
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
|
||||
files.forEach(fn => {
|
||||
const loaded = require(fn).default
|
||||
|
||||
@@ -119,9 +168,10 @@ function registerServices(pluginDetails, container) {
|
||||
* @return {void}
|
||||
*/
|
||||
function registerModels(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/models/*`, {})
|
||||
const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {})
|
||||
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}`
|
||||
@@ -131,7 +181,9 @@ function registerModels(pluginDetails, container) {
|
||||
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asFunction(cradle => new loaded(cradle, pluginDetails.options)),
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -201,15 +253,22 @@ function resolvePlugin(pluginName) {
|
||||
}
|
||||
}
|
||||
|
||||
const rootDir = path.resolve(".")
|
||||
|
||||
/**
|
||||
* Here we have an absolute path to an internal plugin, or a name of a module
|
||||
* which should be located in node_modules.
|
||||
*/
|
||||
try {
|
||||
const requireSource =
|
||||
rootDir !== null
|
||||
? createRequireFromPath(`${rootDir}/:internal:`)
|
||||
: require
|
||||
|
||||
// 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`)
|
||||
requireSource.resolve(`${pluginName}/package.json`)
|
||||
)
|
||||
|
||||
const packageJSON = JSON.parse(
|
||||
@@ -229,3 +288,7 @@ function resolvePlugin(pluginName) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function createFileContentHash(path, files) {
|
||||
return path + files
|
||||
}
|
||||
@@ -1,65 +1,25 @@
|
||||
import { BaseService, PaymentService } from "medusa-interfaces"
|
||||
import glob from "glob"
|
||||
import path from "path"
|
||||
import { Lifetime } from "awilix"
|
||||
import { asFunction } from "awilix"
|
||||
import { Lifetime, asFunction } from "awilix"
|
||||
|
||||
/**
|
||||
* Registers all services in the services directory
|
||||
*/
|
||||
export default ({ container }) => {
|
||||
let corePath = "../services/*.js"
|
||||
let appPath = "src/services/*.js"
|
||||
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
corePath = "../services/__mocks__/*.js"
|
||||
appPath = "src/services/__mocks__/*.js"
|
||||
}
|
||||
const isTest = process.env.NODE_ENV === "test"
|
||||
|
||||
const corePath = isTest ? "../services/__mocks__/*.js" : "../services/*.js"
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
const appFull = path.resolve(appPath)
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach(fn => {
|
||||
const loaded = require(fn).default
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asFunction(cradle => new loaded(cradle)),
|
||||
[name]: asFunction(cradle => new loaded(cradle)).singleton(),
|
||||
})
|
||||
})
|
||||
|
||||
if (coreFull !== appFull) {
|
||||
const files = glob.sync(appFull)
|
||||
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))
|
||||
)
|
||||
|
||||
// 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)),
|
||||
})
|
||||
} else {
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asFunction(cradle => new loaded(cradle)),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function formatRegistrationName(fn) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import mongoose from "mongoose"
|
||||
import { BaseModel } from "medusa-interfaces"
|
||||
import DiscountRule from "./schemas/discount-rule"
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const MiddlewareServiceMock = {
|
||||
usePostAuthentication: jest.fn(),
|
||||
usePreAuthentication: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return MiddlewareServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -0,0 +1,81 @@
|
||||
import mongoose from "mongoose"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import MiddlewareService from "../middleware"
|
||||
|
||||
describe("MiddlewareService", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("addPostAuthentication", () => {
|
||||
const middlewareService = new MiddlewareService()
|
||||
|
||||
it("adds middleware function", () => {
|
||||
middlewareService.addPostAuthentication(() => "post", {})
|
||||
expect(middlewareService.postAuthentication_.length).toEqual(1)
|
||||
})
|
||||
|
||||
it("fails when no function", () => {
|
||||
expect(() => middlewareService.addPostAuthentication("post", {})).toThrow(
|
||||
"Middleware must be a function"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addPreAuthentication", () => {
|
||||
const middlewareService = new MiddlewareService()
|
||||
|
||||
it("adds middleware function", () => {
|
||||
middlewareService.addPreAuthentication(() => "pre", {})
|
||||
expect(middlewareService.preAuthentication_.length).toEqual(1)
|
||||
})
|
||||
|
||||
it("fails when no function", () => {
|
||||
expect(() => middlewareService.addPreAuthentication("pre", {})).toThrow(
|
||||
"Middleware must be a function"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("usePostAuthentication", () => {
|
||||
const middlewareService = new MiddlewareService()
|
||||
|
||||
it("calls middleware", () => {
|
||||
// This doesn't reflect how middleware works but does suffice in our
|
||||
// testing situation
|
||||
const mid = args => args
|
||||
|
||||
middlewareService.addPostAuthentication(mid, { data: "yes" })
|
||||
|
||||
const app = {
|
||||
use: jest.fn(),
|
||||
}
|
||||
|
||||
middlewareService.usePostAuthentication(app)
|
||||
|
||||
expect(app.use).toHaveBeenCalledTimes(1)
|
||||
expect(app.use).toHaveBeenCalledWith({ data: "yes" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("usePreAuthentication", () => {
|
||||
const middlewareService = new MiddlewareService()
|
||||
|
||||
it("calls middleware", () => {
|
||||
// This doesn't reflect how middleware works but does suffice in our
|
||||
// testing situation
|
||||
const mid = args => args
|
||||
|
||||
middlewareService.addPreAuthentication(mid, { data: "yes" })
|
||||
|
||||
const app = {
|
||||
use: jest.fn(),
|
||||
}
|
||||
|
||||
middlewareService.usePreAuthentication(app)
|
||||
|
||||
expect(app.use).toHaveBeenCalledTimes(1)
|
||||
expect(app.use).toHaveBeenCalledWith({ data: "yes" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Orchestrates dynamic middleware registered through the Medusa Middleware API
|
||||
*/
|
||||
class MiddlewareService {
|
||||
constructor(container) {
|
||||
this.postAuthentication_ = []
|
||||
this.preAuthentication_ = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a middleware function, throws if fn is not of type function.
|
||||
* @param {function} fn - the middleware function to validate.
|
||||
*/
|
||||
validateMiddleware_(fn) {
|
||||
if (typeof fn !== "function") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Middleware must be a function"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a middleware function to be called after authentication is completed.
|
||||
* @param {function} middleware - the middleware function. Should return a
|
||||
* middleware function.
|
||||
* @param {object} options - the arguments that will be passed to the
|
||||
* middleware
|
||||
* @return {void}
|
||||
*/
|
||||
addPostAuthentication(middleware, options) {
|
||||
this.validateMiddleware_(middleware)
|
||||
this.postAuthentication_.push({
|
||||
middleware,
|
||||
options: options || {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a middleware function to be called before authentication is completed.
|
||||
* @param {function} middleware - the middleware function. Should return a
|
||||
* middleware function.
|
||||
* @param {object} options - the arguments that will be passed to the
|
||||
* middleware
|
||||
* @return {void}
|
||||
*/
|
||||
addPreAuthentication(middleware, options) {
|
||||
this.validateMiddleware_(middleware)
|
||||
this.preAuthentication_.push({
|
||||
middleware,
|
||||
options: options || {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds post authentication middleware to an express app.
|
||||
* @param {ExpressApp} app - the express app to add the middleware to
|
||||
* @return {void}
|
||||
*/
|
||||
usePostAuthentication(app) {
|
||||
for (const object of this.postAuthentication_) {
|
||||
app.use(object.middleware(object.options))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds pre authentication middleware to an express app.
|
||||
* @param {ExpressApp} app - the express app to add the middleware to
|
||||
* @return {void}
|
||||
*/
|
||||
usePreAuthentication(app) {
|
||||
for (const object of this.preAuthentication_) {
|
||||
app.use(object.middleware(object.options))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MiddlewareService
|
||||
+1331
-810
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user