mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 10:14:08 +03:00
Adds middleware customization (#1092)
This commit is contained in:
parent
6742c5d286
commit
013a13dee2
@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## v0.10.3
|
||||
|
||||
### Express middleware customization
|
||||
We now offer the ability to customize Express middleware:
|
||||
- globally (impacting all actions, queries, and apis by default)
|
||||
- on a per-api basis
|
||||
- on a per-path basis (groups of apis)
|
||||
|
||||
This should make it much easier to work with apis and to customize your Express app in general.
|
||||
|
||||
## v0.10.2
|
||||
|
||||
### Bug fixes
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,47 @@
|
||||
{{={= =}=}}
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
{=# globalMiddlewareConfigFn.isDefined =}
|
||||
{=& globalMiddlewareConfigFn.importStatement =}
|
||||
{=/ globalMiddlewareConfigFn.isDefined =}
|
||||
{=^ globalMiddlewareConfigFn.isDefined =}
|
||||
const {=& globalMiddlewareConfigFn.importAlias =} = (mc: MiddlewareConfig) => mc
|
||||
{=/ globalMiddlewareConfigFn.isDefined =}
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = {=& globalMiddlewareConfigFn.importAlias =}(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -2,22 +2,46 @@
|
||||
import express from 'express'
|
||||
import prisma from '../../dbClient.js'
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js'
|
||||
{=# isAuthEnabled =}
|
||||
import auth from '../../core/auth.js'
|
||||
import { type SanitizedUser } from '../../_types'
|
||||
{=/ isAuthEnabled =}
|
||||
|
||||
{=# apiNamespaces =}
|
||||
{=& namespaceMiddlewareConfigFnImportStatement =}
|
||||
{=/ apiNamespaces =}
|
||||
|
||||
{=# apiRoutes =}
|
||||
{=& importStatement =}
|
||||
{=# routeMiddlewareConfigFn.isDefined =}
|
||||
{=& routeMiddlewareConfigFn.importStatement =}
|
||||
{=/ routeMiddlewareConfigFn.isDefined =}
|
||||
{=/ apiRoutes =}
|
||||
|
||||
const idFn: MiddlewareConfigFn = x => x
|
||||
|
||||
{=# apiRoutes =}
|
||||
{=^ routeMiddlewareConfigFn.isDefined =}
|
||||
const {=& routeMiddlewareConfigFn.importAlias =} = idFn
|
||||
{=/ routeMiddlewareConfigFn.isDefined =}
|
||||
{=/ apiRoutes =}
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
{=# apiNamespaces =}
|
||||
router.use('{= namespacePath =}', globalMiddlewareConfigForExpress({= namespaceMiddlewareConfigFnImportAlias =}))
|
||||
{=/ apiNamespaces =}
|
||||
|
||||
{=# apiRoutes =}
|
||||
const {= apiName =}Middleware = globalMiddlewareConfigForExpress({= routeMiddlewareConfigFn.importAlias =})
|
||||
router.{= routeMethod =}(
|
||||
'{= routePath =}',
|
||||
{=# usesAuth =}
|
||||
auth,
|
||||
[auth, ...{= apiName =}Middleware],
|
||||
{=/ usesAuth =}
|
||||
{=^ usesAuth =}
|
||||
{= apiName =}Middleware,
|
||||
{=/ usesAuth =}
|
||||
handleRejection(
|
||||
(
|
||||
|
@ -1,6 +1,7 @@
|
||||
{{={= =}=}}
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
{=# isAuthEnabled =}
|
||||
import auth from './auth/index.js'
|
||||
{=/ isAuthEnabled =}
|
||||
@ -10,17 +11,20 @@ import apis from './apis/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
{=# isAuthEnabled =}
|
||||
router.use('/auth', auth)
|
||||
router.use('/auth', middleware, auth)
|
||||
{=/ isAuthEnabled =}
|
||||
router.use('/{= operationsRouteInRootRouter =}', operations)
|
||||
router.use('/{= operationsRouteInRootRouter =}', middleware, operations)
|
||||
{=# areThereAnyCustomApiRoutes =}
|
||||
// Keep user-defined api routes last so they cannot override our routes.
|
||||
// NOTE: Keep user-defined api routes last so they cannot override our routes.
|
||||
// Additionally, do not add middleware to these routes here. Instead, we add
|
||||
// it later to allow for middleware customization.
|
||||
router.use(apis)
|
||||
{=/ areThereAnyCustomApiRoutes =}
|
||||
|
||||
|
@ -32,6 +32,7 @@ waspComplexTest = do
|
||||
<++> addAction
|
||||
<++> addQuery
|
||||
<++> addApi
|
||||
<++> addApiNamespace
|
||||
<++> sequence
|
||||
[ waspCliCompile
|
||||
]
|
||||
@ -246,8 +247,8 @@ addApi = do
|
||||
unlines
|
||||
[ "api fooBar {",
|
||||
" fn: import { fooBar } from \"@server/apis.js\",",
|
||||
" httpRoute: (GET, \"/foo/bar\")",
|
||||
" // implicit auth:true",
|
||||
" httpRoute: (GET, \"/foo/bar\"),",
|
||||
" middlewareConfigFn: import { fooBarMiddlewareFn } from \"@server/apis.js\"",
|
||||
"}",
|
||||
"api fooBaz {",
|
||||
" fn: import { fooBaz } from \"@server/apis.js\",",
|
||||
@ -259,12 +260,39 @@ addApi = do
|
||||
apiFile =
|
||||
unlines
|
||||
[ "import { FooBar, FooBaz } from '@wasp/apis/types'",
|
||||
"import { MiddlewareConfigFn } from '@wasp/middleware'",
|
||||
"export const fooBar: FooBar = (req, res, context) => {",
|
||||
" res.set('Access-Control-Allow-Origin', '*')",
|
||||
" res.json({ msg: 'Hello, context.user.username!' })",
|
||||
"}",
|
||||
"export const fooBaz: FooBaz = (req, res, context) => {",
|
||||
" res.json({ msg: 'Hello, stranger!' })",
|
||||
"}",
|
||||
"export const fooBarMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {",
|
||||
" return middlewareConfig",
|
||||
"}"
|
||||
]
|
||||
|
||||
addApiNamespace :: ShellCommandBuilder [ShellCommand]
|
||||
addApiNamespace = do
|
||||
sequence
|
||||
[ appendToWaspFile apiNamespaceDecl,
|
||||
createFile apiNamespaceFile "./src/server" "apiNamespaces.ts"
|
||||
]
|
||||
where
|
||||
apiNamespaceDecl =
|
||||
unlines
|
||||
[ "apiNamespace fooBarNamespace {",
|
||||
" middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from \"@server/apiNamespaces.js\",",
|
||||
" path: \"/bar\"",
|
||||
"}"
|
||||
]
|
||||
|
||||
apiNamespaceFile =
|
||||
unlines
|
||||
[ "import { MiddlewareConfigFn } from '@wasp/middleware'",
|
||||
"export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {",
|
||||
" return middlewareConfig",
|
||||
"}"
|
||||
]
|
||||
|
||||
|
@ -25,6 +25,8 @@ waspBuild/.wasp/build/server/src/jobs/core/allJobs.js
|
||||
waspBuild/.wasp/build/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspBuild/.wasp/build/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspBuild/.wasp/build/server/src/jobs/core/simpleJob.js
|
||||
waspBuild/.wasp/build/server/src/middleware/globalMiddleware.ts
|
||||
waspBuild/.wasp/build/server/src/middleware/index.ts
|
||||
waspBuild/.wasp/build/server/src/queries/types.ts
|
||||
waspBuild/.wasp/build/server/src/routes/index.js
|
||||
waspBuild/.wasp/build/server/src/routes/operations/index.js
|
||||
|
@ -95,7 +95,7 @@
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"f7df4b76a53a92117e0ddca41edd47961cf20ee6f13cc4d252e11c2a293a6e76"
|
||||
"12291f83f685eeb81a8c082961120d4bddb91985092eb142964e554a21d3384c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -181,6 +181,20 @@
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/globalMiddleware.ts"
|
||||
],
|
||||
"87edaddb661fc516406b20e6b7fd60ce178f64e6a94bc7c1d02a85330fff822a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/index.ts"
|
||||
],
|
||||
"e658719309f9375f389c5d8d416fc27f9c247049e61188b3e01df954bcec15a4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -193,7 +207,7 @@
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"7fb59b1d6c05570ca1d42d5dbf5868160844165f04e1c75e4be7c372965063fb"
|
||||
"c34f77a96150414957386f5645c9becb12725c9f231aaaa8db798e3564bd75ce"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const _waspGlobalMiddlewareConfigFn = (mc: MiddlewareConfig) => mc
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = _waspGlobalMiddlewareConfigFn(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -1,13 +1,15 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/operations', operations)
|
||||
router.use('/operations', middleware, operations)
|
||||
|
||||
export default router
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspBuild {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
title: "waspBuild"
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ waspCompile/.wasp/out/server/src/jobs/core/allJobs.js
|
||||
waspCompile/.wasp/out/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspCompile/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspCompile/.wasp/out/server/src/jobs/core/simpleJob.js
|
||||
waspCompile/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspCompile/.wasp/out/server/src/middleware/index.ts
|
||||
waspCompile/.wasp/out/server/src/queries/types.ts
|
||||
waspCompile/.wasp/out/server/src/routes/index.js
|
||||
waspCompile/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -102,7 +102,7 @@
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"f7df4b76a53a92117e0ddca41edd47961cf20ee6f13cc4d252e11c2a293a6e76"
|
||||
"12291f83f685eeb81a8c082961120d4bddb91985092eb142964e554a21d3384c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -188,6 +188,20 @@
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/globalMiddleware.ts"
|
||||
],
|
||||
"87edaddb661fc516406b20e6b7fd60ce178f64e6a94bc7c1d02a85330fff822a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/index.ts"
|
||||
],
|
||||
"e658719309f9375f389c5d8d416fc27f9c247049e61188b3e01df954bcec15a4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -200,7 +214,7 @@
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"7fb59b1d6c05570ca1d42d5dbf5868160844165f04e1c75e4be7c372965063fb"
|
||||
"c34f77a96150414957386f5645c9becb12725c9f231aaaa8db798e3564bd75ce"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const _waspGlobalMiddlewareConfigFn = (mc: MiddlewareConfig) => mc
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = _waspGlobalMiddlewareConfigFn(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -1,13 +1,15 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/operations', operations)
|
||||
router.use('/operations', middleware, operations)
|
||||
|
||||
export default router
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspCompile {
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
title: "waspCompile"
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ waspComplexTest/.wasp/out/server/src/email/core/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/email/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/entities/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/actions/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/apiNamespaces.ts
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/apis.ts
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/jobs/bar.js
|
||||
waspComplexTest/.wasp/out/server/src/ext-src/myServerSetupCode.js
|
||||
@ -55,6 +56,8 @@ waspComplexTest/.wasp/out/server/src/jobs/core/allJobs.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspComplexTest/.wasp/out/server/src/jobs/core/simpleJob.js
|
||||
waspComplexTest/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspComplexTest/.wasp/out/server/src/middleware/index.ts
|
||||
waspComplexTest/.wasp/out/server/src/queries/MySpecialQuery.ts
|
||||
waspComplexTest/.wasp/out/server/src/queries/types.ts
|
||||
waspComplexTest/.wasp/out/server/src/routes/apis/index.ts
|
||||
@ -144,6 +147,7 @@ waspComplexTest/src/client/tsconfig.json
|
||||
waspComplexTest/src/client/vite-env.d.ts
|
||||
waspComplexTest/src/client/waspLogo.png
|
||||
waspComplexTest/src/server/actions/bar.js
|
||||
waspComplexTest/src/server/apiNamespaces.ts
|
||||
waspComplexTest/src/server/apis.ts
|
||||
waspComplexTest/src/server/jobs/bar.js
|
||||
waspComplexTest/src/server/myServerSetupCode.js
|
||||
|
@ -123,7 +123,7 @@
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"f7df4b76a53a92117e0ddca41edd47961cf20ee6f13cc4d252e11c2a293a6e76"
|
||||
"12291f83f685eeb81a8c082961120d4bddb91985092eb142964e554a21d3384c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -293,12 +293,19 @@
|
||||
],
|
||||
"83c606a3eee7608155cdb2c2a20a38f851a82987e060ce25b196b467092c4740"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/apiNamespaces.ts"
|
||||
],
|
||||
"33ea32722151840f2591036f3f548a7af96d5218aea6468ac94fd3c0fe05cb78"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/ext-src/apis.ts"
|
||||
],
|
||||
"09c24033466aa3c0caaf923c4260c87f756d6b4c3bf2c53acd75196a85361ee2"
|
||||
"3d00118d2b80472e7efb24c89d309471ad5e0e9c6212f5dda908eebe8436d99d"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -370,6 +377,20 @@
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/globalMiddleware.ts"
|
||||
],
|
||||
"87edaddb661fc516406b20e6b7fd60ce178f64e6a94bc7c1d02a85330fff822a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/index.ts"
|
||||
],
|
||||
"e658719309f9375f389c5d8d416fc27f9c247049e61188b3e01df954bcec15a4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -389,7 +410,7 @@
|
||||
"file",
|
||||
"server/src/routes/apis/index.ts"
|
||||
],
|
||||
"601591c5b0846be03c31ad70185ba6e6592732656843daf91c5ecc9e1b053fe3"
|
||||
"879cafb5d1a8d2dd58acff9254c44b8449060d68183c49d95f1a141806fc969e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -410,7 +431,7 @@
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"8adccf8d9ca89d67bac22ee3fac02a4bc94dde696388cadb33962cf89372fd73"
|
||||
"57c6074cfb790ea019efbab199e860e70248a3e758419312939ba63c7a84a42c"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { MiddlewareConfigFn } from '../middleware'
|
||||
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
return middlewareConfig
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FooBar, FooBaz } from '../apis/types'
|
||||
import { MiddlewareConfigFn } from '../middleware'
|
||||
export const fooBar: FooBar = (req, res, context) => {
|
||||
res.set('Access-Control-Allow-Origin', '*')
|
||||
res.json({ msg: 'Hello, context.user.username!' })
|
||||
@ -6,4 +7,7 @@ export const fooBar: FooBar = (req, res, context) => {
|
||||
export const fooBaz: FooBaz = (req, res, context) => {
|
||||
res.json({ msg: 'Hello, stranger!' })
|
||||
}
|
||||
export const fooBarMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const _waspGlobalMiddlewareConfigFn = (mc: MiddlewareConfig) => mc
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = _waspGlobalMiddlewareConfigFn(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -1,43 +1,56 @@
|
||||
import express from 'express'
|
||||
import prisma from '../../dbClient.js'
|
||||
import { handleRejection } from '../../utils.js'
|
||||
import { MiddlewareConfigFn, globalMiddlewareConfigForExpress } from '../../middleware/index.js'
|
||||
import auth from '../../core/auth.js'
|
||||
import { type SanitizedUser } from '../../_types'
|
||||
|
||||
import { fooBar } from '../../ext-src/apis.js'
|
||||
import { fooBaz } from '../../ext-src/apis.js'
|
||||
import { fooBarNamespaceMiddlewareFn as _waspfooBarNamespacenamespaceMiddlewareConfigFn } from '../../ext-src/apiNamespaces.js'
|
||||
|
||||
import { fooBar as _waspfooBarfn } from '../../ext-src/apis.js'
|
||||
import { fooBarMiddlewareFn as _waspfooBarmiddlewareConfigFn } from '../../ext-src/apis.js'
|
||||
import { fooBaz as _waspfooBazfn } from '../../ext-src/apis.js'
|
||||
|
||||
const idFn: MiddlewareConfigFn = x => x
|
||||
|
||||
const _waspfooBazmiddlewareConfigFn = idFn
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/bar', globalMiddlewareConfigForExpress(_waspfooBarNamespacenamespaceMiddlewareConfigFn))
|
||||
|
||||
const fooBarMiddleware = globalMiddlewareConfigForExpress(_waspfooBarmiddlewareConfigFn)
|
||||
router.get(
|
||||
'/foo/bar',
|
||||
auth,
|
||||
[auth, ...fooBarMiddleware],
|
||||
handleRejection(
|
||||
(
|
||||
req: Parameters<typeof fooBar>[0] & { user: SanitizedUser },
|
||||
res: Parameters<typeof fooBar>[1],
|
||||
req: Parameters<typeof _waspfooBarfn>[0] & { user: SanitizedUser },
|
||||
res: Parameters<typeof _waspfooBarfn>[1],
|
||||
) => {
|
||||
const context = {
|
||||
user: req.user,
|
||||
entities: {
|
||||
},
|
||||
}
|
||||
return fooBar(req, res, context)
|
||||
return _waspfooBarfn(req, res, context)
|
||||
}
|
||||
)
|
||||
)
|
||||
const fooBazMiddleware = globalMiddlewareConfigForExpress(_waspfooBazmiddlewareConfigFn)
|
||||
router.get(
|
||||
'/foo/baz',
|
||||
fooBazMiddleware,
|
||||
handleRejection(
|
||||
(
|
||||
req: Parameters<typeof fooBaz>[0],
|
||||
res: Parameters<typeof fooBaz>[1],
|
||||
req: Parameters<typeof _waspfooBazfn>[0],
|
||||
res: Parameters<typeof _waspfooBazfn>[1],
|
||||
) => {
|
||||
const context = {
|
||||
entities: {
|
||||
},
|
||||
}
|
||||
return fooBaz(req, res, context)
|
||||
return _waspfooBazfn(req, res, context)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -1,18 +1,22 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
import auth from './auth/index.js'
|
||||
import apis from './apis/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/auth', auth)
|
||||
router.use('/operations', operations)
|
||||
// Keep user-defined api routes last so they cannot override our routes.
|
||||
router.use('/auth', middleware, auth)
|
||||
router.use('/operations', middleware, operations)
|
||||
// NOTE: Keep user-defined api routes last so they cannot override our routes.
|
||||
// Additionally, do not add middleware to these routes here. Instead, we add
|
||||
// it later to allow for middleware customization.
|
||||
router.use(apis)
|
||||
|
||||
export default router
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspComplexTest {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
auth: {
|
||||
userEntity: User,
|
||||
@ -77,8 +77,8 @@ query MySpecialQuery {
|
||||
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
// implicit auth:true
|
||||
httpRoute: (GET, "/foo/bar"),
|
||||
middlewareConfigFn: import { fooBarMiddlewareFn } from "@server/apis.js"
|
||||
}
|
||||
api fooBaz {
|
||||
fn: import { fooBaz } from "@server/apis.js",
|
||||
@ -86,3 +86,8 @@ api fooBaz {
|
||||
auth: false
|
||||
}
|
||||
|
||||
apiNamespace fooBarNamespace {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apiNamespaces.js",
|
||||
path: "/bar"
|
||||
}
|
||||
|
||||
|
5
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/src/server/apiNamespaces.ts
generated
Normal file
5
waspc/e2e-test/test-outputs/waspComplexTest-golden/waspComplexTest/src/server/apiNamespaces.ts
generated
Normal file
@ -0,0 +1,5 @@
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
return middlewareConfig
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FooBar, FooBaz } from '@wasp/apis/types'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
export const fooBar: FooBar = (req, res, context) => {
|
||||
res.set('Access-Control-Allow-Origin', '*')
|
||||
res.json({ msg: 'Hello, context.user.username!' })
|
||||
@ -6,4 +7,7 @@ export const fooBar: FooBar = (req, res, context) => {
|
||||
export const fooBaz: FooBaz = (req, res, context) => {
|
||||
res.json({ msg: 'Hello, stranger!' })
|
||||
}
|
||||
export const fooBarMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,8 @@ waspJob/.wasp/out/server/src/jobs/core/allJobs.js
|
||||
waspJob/.wasp/out/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspJob/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspJob/.wasp/out/server/src/jobs/core/simpleJob.js
|
||||
waspJob/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspJob/.wasp/out/server/src/middleware/index.ts
|
||||
waspJob/.wasp/out/server/src/queries/types.ts
|
||||
waspJob/.wasp/out/server/src/routes/index.js
|
||||
waspJob/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -102,7 +102,7 @@
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"f7df4b76a53a92117e0ddca41edd47961cf20ee6f13cc4d252e11c2a293a6e76"
|
||||
"12291f83f685eeb81a8c082961120d4bddb91985092eb142964e554a21d3384c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -202,6 +202,20 @@
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/globalMiddleware.ts"
|
||||
],
|
||||
"87edaddb661fc516406b20e6b7fd60ce178f64e6a94bc7c1d02a85330fff822a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/index.ts"
|
||||
],
|
||||
"e658719309f9375f389c5d8d416fc27f9c247049e61188b3e01df954bcec15a4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -214,7 +228,7 @@
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"7fb59b1d6c05570ca1d42d5dbf5868160844165f04e1c75e4be7c372965063fb"
|
||||
"c34f77a96150414957386f5645c9becb12725c9f231aaaa8db798e3564bd75ce"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const _waspGlobalMiddlewareConfigFn = (mc: MiddlewareConfig) => mc
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = _waspGlobalMiddlewareConfigFn(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
1
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/middleware/index.ts
generated
Normal file
1
waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/middleware/index.ts
generated
Normal file
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -1,13 +1,15 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/operations', operations)
|
||||
router.use('/operations', middleware, operations)
|
||||
|
||||
export default router
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspJob {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
title: "waspJob"
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ waspMigrate/.wasp/out/server/src/jobs/core/allJobs.js
|
||||
waspMigrate/.wasp/out/server/src/jobs/core/pgBoss/pgBoss.js
|
||||
waspMigrate/.wasp/out/server/src/jobs/core/pgBoss/pgBossJob.js
|
||||
waspMigrate/.wasp/out/server/src/jobs/core/simpleJob.js
|
||||
waspMigrate/.wasp/out/server/src/middleware/globalMiddleware.ts
|
||||
waspMigrate/.wasp/out/server/src/middleware/index.ts
|
||||
waspMigrate/.wasp/out/server/src/queries/types.ts
|
||||
waspMigrate/.wasp/out/server/src/routes/index.js
|
||||
waspMigrate/.wasp/out/server/src/routes/operations/index.js
|
||||
|
@ -102,7 +102,7 @@
|
||||
"file",
|
||||
"server/src/app.js"
|
||||
],
|
||||
"f7df4b76a53a92117e0ddca41edd47961cf20ee6f13cc4d252e11c2a293a6e76"
|
||||
"12291f83f685eeb81a8c082961120d4bddb91985092eb142964e554a21d3384c"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -188,6 +188,20 @@
|
||||
],
|
||||
"36fe173d9f5128859196bfd3a661983df2d95eb34d165a469b840982b06cf59b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/globalMiddleware.ts"
|
||||
],
|
||||
"87edaddb661fc516406b20e6b7fd60ce178f64e6a94bc7c1d02a85330fff822a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"server/src/middleware/index.ts"
|
||||
],
|
||||
"e658719309f9375f389c5d8d416fc27f9c247049e61188b3e01df954bcec15a4"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -200,7 +214,7 @@
|
||||
"file",
|
||||
"server/src/routes/index.js"
|
||||
],
|
||||
"7fb59b1d6c05570ca1d42d5dbf5868160844165f04e1c75e4be7c372965063fb"
|
||||
"c34f77a96150414957386f5645c9becb12725c9f231aaaa8db798e3564bd75ce"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -1,32 +1,19 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import HttpError from './core/HttpError.js'
|
||||
import indexRouter from './routes/index.js'
|
||||
import config from './config.js'
|
||||
|
||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||
// it can be used in unit tests to test each route individually.
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
// TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
|
||||
origin: config.allowedCORSOrigins,
|
||||
}))
|
||||
app.use(logger('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(cookieParser())
|
||||
// NOTE: Middleware are installed on a per-router or per-route basis.
|
||||
|
||||
app.use('/', indexRouter)
|
||||
|
||||
// Custom error handler.
|
||||
app.use((err, req, res, next) => {
|
||||
app.use((err, _req, res, next) => {
|
||||
// As by expressjs documentation, when the headers have already
|
||||
// been sent to the client, we must delegate to the default error handler.
|
||||
if (res.headersSent) { return next(err) }
|
||||
|
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import logger from 'morgan'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import config from '../config.js'
|
||||
|
||||
const _waspGlobalMiddlewareConfigFn = (mc: MiddlewareConfig) => mc
|
||||
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
// This is the set of middleware Wasp supplies by default.
|
||||
// NOTE: Remember to update the docs of these change.
|
||||
const defaultGlobalMiddlewareConfig: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
|
||||
// This is the global middleware that is the result of applying the user's modifications.
|
||||
// It will be used as the basis for Operations and APIs (unless they are further customized).
|
||||
const globalMiddlewareConfig = _waspGlobalMiddlewareConfigFn(defaultGlobalMiddlewareConfig)
|
||||
|
||||
// This function returns an array of Express middleware to be used by a router. It optionally
|
||||
// accepts a function that can modify the global middleware for specific route customization.
|
||||
export function globalMiddlewareConfigForExpress(middlewareConfigFn?: MiddlewareConfigFn): express.RequestHandler[] {
|
||||
if (!middlewareConfigFn) {
|
||||
return Array.from(globalMiddlewareConfig.values())
|
||||
}
|
||||
|
||||
// Make a clone so they can't mess up the global Map for any other routes calling this.
|
||||
const globalMiddlewareConfigClone = new Map(globalMiddlewareConfig)
|
||||
const modifiedMiddlewareConfig = middlewareConfigFn(globalMiddlewareConfigClone)
|
||||
return Array.from(modifiedMiddlewareConfig.values())
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './globalMiddleware.js'
|
@ -1,13 +1,15 @@
|
||||
import express from 'express'
|
||||
import operations from './operations/index.js'
|
||||
import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
|
||||
|
||||
|
||||
const router = express.Router()
|
||||
const middleware = globalMiddlewareConfigForExpress()
|
||||
|
||||
router.get('/', function (req, res, next) {
|
||||
router.get('/', middleware, function (_req, res, _next) {
|
||||
res.json('Hello world')
|
||||
})
|
||||
|
||||
router.use('/operations', operations)
|
||||
router.use('/operations', middleware, operations)
|
||||
|
||||
export default router
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspMigrate {
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
title: "waspMigrate"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspNew {
|
||||
wasp: {
|
||||
version: "^0.10.2"
|
||||
version: "^0.10.3"
|
||||
},
|
||||
title: "waspNew"
|
||||
}
|
||||
|
@ -1,11 +1,50 @@
|
||||
import { BarBaz, FooBar } from '@wasp/apis/types'
|
||||
import { BarBaz, FooBar, WebhookCallback } from '@wasp/apis/types'
|
||||
import express from 'express'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const fooBar: FooBar = (_req, res, context) => {
|
||||
res.set('Access-Control-Allow-Origin', '*') // Example of modifying headers to override Wasp default CORS middleware.
|
||||
res.json({ msg: `Hello, ${context.user.email}!` })
|
||||
res.json({ msg: `Hello, ${context?.user?.email}!` })
|
||||
}
|
||||
|
||||
export const fooBarMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// console.log('fooBarMiddlewareFn: Adding custom middleware for route.')
|
||||
|
||||
const customMiddleware : express.RequestHandler = (_req, _res, next) => {
|
||||
console.log('fooBarMiddlewareFn: custom route middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.route', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export const barBaz: BarBaz = (_req, res, _context) => {
|
||||
res.set('Access-Control-Allow-Origin', '*')
|
||||
res.json({ msg: `Hello, stranger!` })
|
||||
}
|
||||
|
||||
export const barNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
console.log('barNamespaceMiddlewareFn: Ignoring all default middleware.')
|
||||
|
||||
middlewareConfig.set('custom.apiNamespace',
|
||||
(req, _res, next) => {
|
||||
console.log(`barNamespaceMiddlewareFn: custom middleware (path: ${req.path})`)
|
||||
next()
|
||||
}
|
||||
)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export const webhookCallback: WebhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { mySpecialJob } from '@wasp/jobs/mySpecialJob.js'
|
||||
import { sayHi } from '../shared/util.js'
|
||||
import { ServerSetupFn, Application } from '@wasp/types'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
import cors from 'cors'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
let someResource = undefined
|
||||
|
||||
@ -29,4 +32,10 @@ function addCustomRoute(app: Application) {
|
||||
})
|
||||
}
|
||||
|
||||
export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// Example of adding an extra domain to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'http://127.0.0.1:3000'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
export default setup
|
||||
|
@ -41,7 +41,8 @@ app todoApp {
|
||||
onAuthSucceededRedirectTo: "/profile"
|
||||
},
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js"
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
client: {
|
||||
rootComponent: import { App } from "@client/App.tsx",
|
||||
@ -153,8 +154,15 @@ query getTasks {
|
||||
|
||||
api fooBar {
|
||||
fn: import { fooBar } from "@server/apis.js",
|
||||
middlewareConfigFn: import { fooBarMiddlewareFn } from "@server/apis.js",
|
||||
entities: [Task],
|
||||
httpRoute: (GET, "/foo/bar")
|
||||
// ALL here let's our CORS work. If we did GET, we would need an apiNamespace over it with CORS.
|
||||
httpRoute: (ALL, "/foo/bar")
|
||||
}
|
||||
|
||||
apiNamespace bar {
|
||||
middlewareConfigFn: import { barNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/bar"
|
||||
}
|
||||
|
||||
api barBaz {
|
||||
@ -164,6 +172,13 @@ api barBaz {
|
||||
httpRoute: (GET, "/bar/baz")
|
||||
}
|
||||
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
|
||||
query getNumTasks {
|
||||
fn: import { getNumTasks } from "@server/queries.js",
|
||||
entities: [Task],
|
||||
|
@ -12,6 +12,7 @@ import qualified Wasp.Analyzer.TypeDefinitions as TD
|
||||
import Wasp.Analyzer.TypeDefinitions.TH (makeDeclType, makeEnumType)
|
||||
import Wasp.AppSpec.Action (Action)
|
||||
import Wasp.AppSpec.Api (Api, HttpMethod)
|
||||
import Wasp.AppSpec.ApiNamespace (ApiNamespace)
|
||||
import Wasp.AppSpec.App (App)
|
||||
import Wasp.AppSpec.App.Db (DbSystem)
|
||||
import Wasp.AppSpec.App.EmailSender (EmailProvider)
|
||||
@ -31,6 +32,7 @@ makeEnumType ''JobExecutor
|
||||
makeDeclType ''Job
|
||||
makeEnumType ''HttpMethod
|
||||
makeDeclType ''Api
|
||||
makeDeclType ''ApiNamespace
|
||||
makeDeclType ''App
|
||||
|
||||
{- ORMOLU_DISABLE -}
|
||||
@ -50,6 +52,7 @@ stdTypes =
|
||||
TD.addDeclType @Job $
|
||||
TD.addEnumType @HttpMethod $
|
||||
TD.addDeclType @Api $
|
||||
TD.addDeclType @ApiNamespace $
|
||||
TD.addEnumType @EmailProvider $
|
||||
TD.empty
|
||||
{- ORMOLU_ENABLE -}
|
||||
|
@ -18,6 +18,7 @@ module Wasp.AppSpec
|
||||
doesConfigFileExist,
|
||||
asAbsWaspProjectDirFile,
|
||||
getApp,
|
||||
getApiNamespaces,
|
||||
)
|
||||
where
|
||||
|
||||
@ -27,6 +28,7 @@ import Data.Text (Text)
|
||||
import StrongPath (Abs, Dir, File', Path', Rel, (</>))
|
||||
import Wasp.AppSpec.Action (Action)
|
||||
import Wasp.AppSpec.Api (Api)
|
||||
import Wasp.AppSpec.ApiNamespace (ApiNamespace)
|
||||
import Wasp.AppSpec.App (App)
|
||||
import Wasp.AppSpec.ConfigFile (ConfigFileRelocator (..))
|
||||
import Wasp.AppSpec.Core.Decl (Decl, IsDecl, takeDecls)
|
||||
@ -91,6 +93,9 @@ getActions = getDecls
|
||||
getApis :: AppSpec -> [(String, Api)]
|
||||
getApis = getDecls
|
||||
|
||||
getApiNamespaces :: AppSpec -> [(String, ApiNamespace)]
|
||||
getApiNamespaces = getDecls
|
||||
|
||||
getEntities :: AppSpec -> [(String, Entity)]
|
||||
getEntities = getDecls
|
||||
|
||||
|
@ -16,6 +16,7 @@ import Wasp.AppSpec.ExtImport (ExtImport)
|
||||
|
||||
data Api = Api
|
||||
{ fn :: ExtImport,
|
||||
middlewareConfigFn :: Maybe ExtImport,
|
||||
entities :: Maybe [Ref Entity],
|
||||
httpRoute :: (HttpMethod, String), -- (method, path), exe: (GET, "/foo/bar")
|
||||
auth :: Maybe Bool
|
||||
@ -31,4 +32,4 @@ path :: Api -> String
|
||||
path = snd . httpRoute
|
||||
|
||||
data HttpMethod = ALL | GET | POST | PUT | DELETE
|
||||
deriving (Show, Eq, Data)
|
||||
deriving (Show, Eq, Ord, Data)
|
||||
|
18
waspc/src/Wasp/AppSpec/ApiNamespace.hs
Normal file
18
waspc/src/Wasp/AppSpec/ApiNamespace.hs
Normal file
@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
|
||||
module Wasp.AppSpec.ApiNamespace
|
||||
( ApiNamespace (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Data (Data)
|
||||
import Wasp.AppSpec.Core.Decl (IsDecl)
|
||||
import Wasp.AppSpec.ExtImport (ExtImport)
|
||||
|
||||
data ApiNamespace = ApiNamespace
|
||||
{ middlewareConfigFn :: ExtImport,
|
||||
path :: String
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
instance IsDecl ApiNamespace
|
@ -9,6 +9,7 @@ import Data.Data (Data)
|
||||
import Wasp.AppSpec.ExtImport (ExtImport)
|
||||
|
||||
data Server = Server
|
||||
{ setupFn :: Maybe ExtImport
|
||||
{ setupFn :: Maybe ExtImport,
|
||||
middlewareConfigFn :: Maybe ExtImport
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
@ -10,12 +10,14 @@ module Wasp.AppSpec.Valid
|
||||
where
|
||||
|
||||
import Control.Monad (unless)
|
||||
import Data.List (find)
|
||||
import Data.List (find, group, groupBy, intercalate, sort, sortBy)
|
||||
import Data.Maybe (isJust)
|
||||
import Text.Read (readMaybe)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.Api as AS.Api
|
||||
import qualified Wasp.AppSpec.ApiNamespace as AS.ApiNamespace
|
||||
import Wasp.AppSpec.App (App)
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App as App
|
||||
@ -50,7 +52,9 @@ validateAppSpec spec =
|
||||
validateAuthUserEntityHasCorrectFieldsIfEmailAuthIsUsed spec,
|
||||
validateEmailSenderIsDefinedIfEmailAuthIsUsed spec,
|
||||
validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec,
|
||||
validateDbIsPostgresIfPgBossUsed spec
|
||||
validateDbIsPostgresIfPgBossUsed spec,
|
||||
validateApiRoutesAreUnique spec,
|
||||
validateApiNamespacePathsAreUnique spec
|
||||
]
|
||||
|
||||
validateExactlyOneAppExists :: AppSpec -> Maybe ValidationError
|
||||
@ -217,6 +221,35 @@ validateEntityHasField entityName entityFields (fieldName, fieldType, fieldTypeN
|
||||
"Expected an Entity referenced by " ++ entityName ++ " to have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
|
||||
]
|
||||
|
||||
validateApiRoutesAreUnique :: AppSpec -> [ValidationError]
|
||||
validateApiRoutesAreUnique spec =
|
||||
if null groupsOfConflictingRoutes
|
||||
then []
|
||||
else [GenericValidationError $ "`api` routes must be unique. Duplicates: " ++ intercalate ", " (show <$> groupsOfConflictingRoutes)]
|
||||
where
|
||||
apiRoutes = AS.Api.httpRoute . snd <$> AS.getApis spec
|
||||
groupsOfConflictingRoutes = filter ((> 1) . length) (groupBy routesHaveConflictingDefinitions $ sortBy routeComparator apiRoutes)
|
||||
|
||||
routeComparator :: (AS.Api.HttpMethod, String) -> (AS.Api.HttpMethod, String) -> Ordering
|
||||
routeComparator l r | routesHaveConflictingDefinitions l r = EQ
|
||||
routeComparator l r = compare l r
|
||||
|
||||
-- Two routes have conflicting definitions if they define the same thing twice,
|
||||
-- so we don't know which definition to use. This can happen if they are exactly
|
||||
-- the same (path and method) or if they have the same paths and one has ALL for a method.
|
||||
routesHaveConflictingDefinitions :: (AS.Api.HttpMethod, String) -> (AS.Api.HttpMethod, String) -> Bool
|
||||
routesHaveConflictingDefinitions (lMethod, lPath) (rMethod, rPath) =
|
||||
lPath == rPath && (lMethod == rMethod || AS.Api.ALL `elem` [lMethod, rMethod])
|
||||
|
||||
validateApiNamespacePathsAreUnique :: AppSpec -> [ValidationError]
|
||||
validateApiNamespacePathsAreUnique spec =
|
||||
if null duplicatePaths
|
||||
then []
|
||||
else [GenericValidationError $ "`apiNamespace` paths must be unique. Duplicates: " ++ intercalate ", " duplicatePaths]
|
||||
where
|
||||
namespacePaths = AS.ApiNamespace.path . snd <$> AS.getApiNamespaces spec
|
||||
duplicatePaths = map head $ filter ((> 1) . length) (group . sort $ namespacePaths)
|
||||
|
||||
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
|
||||
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
|
||||
-- Check https://github.com/wasp-lang/wasp/pull/455 for considerations on this and analysis of different approaches.
|
||||
|
@ -62,7 +62,7 @@ import Wasp.Generator.ServerGenerator.Db.Seed (genDbSeed, getPackageJsonPrismaSe
|
||||
import Wasp.Generator.ServerGenerator.EmailSenderG (depsRequiredByEmail, genEmailSender)
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy)
|
||||
import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs)
|
||||
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson)
|
||||
import Wasp.Generator.ServerGenerator.JsImport (extImportToImportJson, getAliasedJsImportStmtAndIdentifier)
|
||||
import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
|
||||
import Wasp.Generator.ServerGenerator.OperationsRoutesG (genOperationsRoutes)
|
||||
import Wasp.Project.Db (databaseUrlEnvVarName)
|
||||
@ -215,6 +215,7 @@ genSrcDir spec =
|
||||
<++> genAuth spec
|
||||
<++> genEmailSender spec
|
||||
<++> genDbSeed spec
|
||||
<++> genMiddleware spec
|
||||
where
|
||||
genFileCopy = return . C.mkSrcTmplFd
|
||||
|
||||
@ -251,8 +252,8 @@ genServerJs spec =
|
||||
where
|
||||
maybeSetupJsFunction = AS.App.Server.setupFn =<< AS.App.server (snd $ getApp spec)
|
||||
|
||||
relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathToServerSrcDir = [reldirP|./|]
|
||||
relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathToServerSrcDir = [reldirP|./|]
|
||||
|
||||
genRoutesDir :: AppSpec -> Generator [FileDraft]
|
||||
genRoutesDir spec =
|
||||
@ -376,3 +377,26 @@ genExportedTypesDir spec =
|
||||
isExternalAuthEnabled = AS.App.Auth.isExternalAuthEnabled <$> maybeAuth
|
||||
isEmailAuthEnabled = AS.App.Auth.isEmailAuthEnabled <$> maybeAuth
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
|
||||
genMiddleware :: AppSpec -> Generator [FileDraft]
|
||||
genMiddleware spec =
|
||||
return
|
||||
[ C.mkTmplFd [relfile|src/middleware/index.ts|],
|
||||
C.mkTmplFdWithData [relfile|src/middleware/globalMiddleware.ts|] (Just tmplData)
|
||||
]
|
||||
where
|
||||
tmplData =
|
||||
object
|
||||
[ "globalMiddlewareConfigFn" .= globalMiddlewareConfigFnTmplData
|
||||
]
|
||||
|
||||
globalMiddlewareConfigFnTmplData :: Aeson.Value
|
||||
globalMiddlewareConfigFnTmplData =
|
||||
let maybeGlobalMiddlewareConfigFn = AS.App.server (snd $ getApp spec) >>= AS.App.Server.middlewareConfigFn
|
||||
globalMiddlewareConfigFnAlias = "_waspGlobalMiddlewareConfigFn"
|
||||
maybeGlobalMidlewareConfigFnImports = getAliasedJsImportStmtAndIdentifier globalMiddlewareConfigFnAlias [reldirP|../|] <$> maybeGlobalMiddlewareConfigFn
|
||||
in object
|
||||
[ "isDefined" .= isJust maybeGlobalMidlewareConfigFnImports,
|
||||
"importStatement" .= maybe "" fst maybeGlobalMidlewareConfigFnImports,
|
||||
"importAlias" .= globalMiddlewareConfigFnAlias
|
||||
]
|
||||
|
@ -7,20 +7,19 @@ import Data.Aeson (object, (.=))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Char (toLower)
|
||||
import Data.List (nub)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import StrongPath (Dir, File', Path, Path', Posix, Rel, reldirP, relfile)
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.AppSpec (AppSpec, getApis)
|
||||
import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.Api as Api
|
||||
import qualified Wasp.AppSpec.App as App
|
||||
import qualified Wasp.AppSpec.App.Auth as App.Auth
|
||||
import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
|
||||
import qualified Wasp.AppSpec.ApiNamespace as ApiNamespace
|
||||
import Wasp.AppSpec.Valid (isAuthEnabled)
|
||||
import Wasp.Generator.Common (ServerRootDir, makeJsonWithEntityData)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.JsImport (getJsImportStmtAndIdentifier)
|
||||
import Wasp.Generator.ServerGenerator.JsImport (getAliasedJsImportStmtAndIdentifier)
|
||||
import Wasp.Util (toUpperFirst)
|
||||
|
||||
genApis :: AppSpec -> Generator [FileDraft]
|
||||
@ -39,30 +38,55 @@ genApiRoutes :: AppSpec -> Generator FileDraft
|
||||
genApiRoutes spec =
|
||||
return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
apis = map snd $ AS.getApis spec
|
||||
namedApis = AS.getApis spec
|
||||
namedNamespaces = AS.getApiNamespaces spec
|
||||
tmplData =
|
||||
object
|
||||
[ "apiRoutes" .= map getTmplData apis,
|
||||
"isAuthEnabled" .= isAuthEnabledGlobally spec,
|
||||
"userEntityName" .= maybe "" (AS.refName . App.Auth.userEntity) (App.auth $ snd $ getApp spec)
|
||||
[ "apiRoutes" .= map getApiRoutesTmplData namedApis,
|
||||
"apiNamespaces" .= map getNamespaceTmplData namedNamespaces,
|
||||
"isAuthEnabled" .= isAuthEnabledGlobally spec
|
||||
]
|
||||
tmplFile = C.asTmplFile [relfile|src/routes/apis/index.ts|]
|
||||
dstFile = SP.castRel tmplFile :: Path' (Rel ServerRootDir) File'
|
||||
|
||||
getTmplData :: Api.Api -> Aeson.Value
|
||||
getTmplData api =
|
||||
let (jsImportStmt, jsImportIdentifier) = getJsImportStmtAndIdentifier relPathFromApisRoutesToServerSrcDir (Api.fn api)
|
||||
in object
|
||||
[ "routeMethod" .= map toLower (show $ Api.method api),
|
||||
"routePath" .= Api.path api,
|
||||
"importStatement" .= jsImportStmt,
|
||||
"importIdentifier" .= jsImportIdentifier,
|
||||
"entities" .= getApiEntitiesObject api,
|
||||
"usesAuth" .= isAuthEnabledForApi spec api
|
||||
]
|
||||
getNamespaceTmplData :: (String, ApiNamespace.ApiNamespace) -> Aeson.Value
|
||||
getNamespaceTmplData (namespaceName, namespace) =
|
||||
object
|
||||
[ "namespacePath" .= ApiNamespace.path namespace,
|
||||
"namespaceMiddlewareConfigFnImportStatement" .= middlewareConfigFnImport,
|
||||
"namespaceMiddlewareConfigFnImportAlias" .= middlewareConfigFnAlias
|
||||
]
|
||||
where
|
||||
relPathFromApisRoutesToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathFromApisRoutesToServerSrcDir = [reldirP|../..|]
|
||||
namespaceConfigFnAlias = "_wasp" ++ namespaceName ++ "namespaceMiddlewareConfigFn"
|
||||
(middlewareConfigFnImport, middlewareConfigFnAlias) = getAliasedJsImportStmtAndIdentifier namespaceConfigFnAlias relPathFromApisRoutesToServerSrcDir (ApiNamespace.middlewareConfigFn namespace)
|
||||
|
||||
getApiRoutesTmplData :: (String, Api.Api) -> Aeson.Value
|
||||
getApiRoutesTmplData (apiName, api) =
|
||||
object
|
||||
[ "routeMethod" .= map toLower (show $ Api.method api),
|
||||
"routePath" .= Api.path api,
|
||||
"importStatement" .= jsImportStmt,
|
||||
"importIdentifier" .= jsImportIdentifier,
|
||||
"entities" .= getApiEntitiesObject api,
|
||||
"usesAuth" .= isAuthEnabledForApi spec api,
|
||||
"routeMiddlewareConfigFn" .= middlewareConfigFnTmplData,
|
||||
"apiName" .= apiName
|
||||
]
|
||||
where
|
||||
(jsImportStmt, jsImportIdentifier) = getAliasedJsImportStmtAndIdentifier ("_wasp" ++ apiName ++ "fn") relPathFromApisRoutesToServerSrcDir (Api.fn api)
|
||||
|
||||
middlewareConfigFnTmplData :: Aeson.Value
|
||||
middlewareConfigFnTmplData =
|
||||
let middlewareConfigFnAlias = "_wasp" ++ apiName ++ "middlewareConfigFn"
|
||||
maybeMiddlewareConfigFnImport = getAliasedJsImportStmtAndIdentifier middlewareConfigFnAlias relPathFromApisRoutesToServerSrcDir <$> Api.middlewareConfigFn api
|
||||
in object
|
||||
[ "isDefined" .= isJust maybeMiddlewareConfigFnImport,
|
||||
"importStatement" .= maybe "" fst maybeMiddlewareConfigFnImport,
|
||||
"importAlias" .= middlewareConfigFnAlias
|
||||
]
|
||||
|
||||
relPathFromApisRoutesToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathFromApisRoutesToServerSrcDir = [reldirP|../..|]
|
||||
|
||||
genApiTypes :: AppSpec -> Generator FileDraft
|
||||
genApiTypes spec =
|
||||
|
@ -10,6 +10,7 @@ import Wasp.Generator.ServerGenerator.Common (ServerSrcDir)
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir)
|
||||
import Wasp.JsImport
|
||||
( JsImport,
|
||||
JsImportAlias,
|
||||
JsImportIdentifier,
|
||||
JsImportStatement,
|
||||
)
|
||||
@ -29,6 +30,14 @@ getJsImportStmtAndIdentifier ::
|
||||
(JsImportStatement, JsImportIdentifier)
|
||||
getJsImportStmtAndIdentifier pathFromImportLocationToExtCodeDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToExtCodeDir
|
||||
|
||||
getAliasedJsImportStmtAndIdentifier ::
|
||||
JsImportAlias ->
|
||||
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
(JsImportStatement, JsImportIdentifier)
|
||||
getAliasedJsImportStmtAndIdentifier importAlias pathFromImportLocationToExtCodeDir =
|
||||
JI.getJsImportStmtAndIdentifier . JI.applyJsImportAlias (Just importAlias) . extImportToJsImport pathFromImportLocationToExtCodeDir
|
||||
|
||||
extImportToJsImport ::
|
||||
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
|
@ -152,7 +152,8 @@ spec_Analyzer = do
|
||||
Just $
|
||||
ExtImport
|
||||
(ExtImportField "setupServer")
|
||||
(fromJust $ SP.parseRelFileP "bar.js")
|
||||
(fromJust $ SP.parseRelFileP "bar.js"),
|
||||
Server.middlewareConfigFn = Nothing
|
||||
},
|
||||
App.client =
|
||||
Just
|
||||
|
@ -6,7 +6,7 @@ cabal-version: 2.4
|
||||
-- Consider using hpack, or maybe even hpack-dhall.
|
||||
|
||||
name: waspc
|
||||
version: 0.10.2
|
||||
version: 0.10.3
|
||||
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
|
||||
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
||||
bug-reports: https://github.com/wasp-lang/wasp/issues
|
||||
@ -185,6 +185,7 @@ library
|
||||
Wasp.AppSpec
|
||||
Wasp.AppSpec.Action
|
||||
Wasp.AppSpec.Api
|
||||
Wasp.AppSpec.ApiNamespace
|
||||
Wasp.AppSpec.App
|
||||
Wasp.AppSpec.App.Auth
|
||||
Wasp.AppSpec.App.Auth.PasswordReset
|
||||
|
145
web/docs/guides/middleware-customization.md
Normal file
145
web/docs/guides/middleware-customization.md
Normal file
@ -0,0 +1,145 @@
|
||||
---
|
||||
title: Middleware Customization
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
# Customizing Express server middleware
|
||||
|
||||
Wasp comes with a minimal set of useful Express middleware in every application. While this is good for most users, we realize some may wish to add, modify, or remove some of these choices both globally, or on a per-`api`/path basis.
|
||||
|
||||
## Default global middleware
|
||||
|
||||
- [Helmet](https://helmetjs.github.io/): Helmet helps you secure your Express apps by setting various HTTP headers. It's not a silver bullet, but it can help!
|
||||
- [CORS](https://github.com/expressjs/cors#readme): CORS is a package for providing a middleware that can be used to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) with various options.
|
||||
- ⚠️ This is required for the frontend to communicate with the backend.
|
||||
- [Morgan](https://github.com/expressjs/morgan#readme): HTTP request logger middleware.
|
||||
- [express.json](https://expressjs.com/en/api.html#express.json) (which uses [body-parser](https://github.com/expressjs/body-parser#bodyparserjsonoptions)): Parses incoming request bodies in a middleware before your handlers, making the result available under the `req.body` property.
|
||||
- ⚠️ This is required for Wasp Operations to function properly.
|
||||
- [express.urlencoded](https://expressjs.com/en/api.html#express.urlencoded) (which uses [body-parser](https://expressjs.com/en/resources/middleware/body-parser.html#bodyparserurlencodedoptions)): Returns middleware that only parses urlencoded bodies and only looks at requests where the `Content-Type` header matches the type option.
|
||||
- [cookieParser](https://github.com/expressjs/cookie-parser#readme): Parse Cookie header and populate `req.cookies` with an object keyed by the cookie names.
|
||||
|
||||
## Customization
|
||||
|
||||
You have three places where you can customize middleware:
|
||||
1. global: here, any changes will apply by default *to all operations (`query` and `action`) and `api`.* This is helpful if you wanted to add support for multiple domains to CORS, for example. ⚠️ Please treat modifications to global middleware with extreme care!
|
||||
2. per-api: you can override middleware for a specific api route (exe: `POST /webhook/callback`). This is helpful if you want to disable JSON parsing, for example.
|
||||
3. per-path: this is helpful if you need to customize middleware for all methods for a given path. This is helpful for things like "complex CORS requests" which may need to apply to both `OPTIONS` and `GET`, or to apply some middleware to a _set of `api` routes_.
|
||||
|
||||
### Types
|
||||
|
||||
Below are the relevant TS types and the actual definitions of default middleware.
|
||||
|
||||
```ts
|
||||
export type MiddlewareConfig = Map<string, express.RequestHandler>
|
||||
|
||||
export type MiddlewareConfigFn = (middlewareConfig: MiddlewareConfig) => MiddlewareConfig
|
||||
|
||||
const defaultGlobalMiddleware: MiddlewareConfig = new Map([
|
||||
['helmet', helmet()],
|
||||
['cors', cors({ origin: config.allowedCORSOrigins })],
|
||||
['logger', logger('dev')],
|
||||
['express.json', express.json()],
|
||||
['express.urlencoded', express.urlencoded({ extended: false })],
|
||||
['cookieParser', cookieParser()]
|
||||
])
|
||||
```
|
||||
|
||||
## 1. Customize global middleware
|
||||
|
||||
If you would like to modify the middleware for _all_ operations and APIs, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
app todoApp {
|
||||
// ...
|
||||
|
||||
server: {
|
||||
setupFn: import setup from "@server/serverSetup.js",
|
||||
middlewareConfigFn: import { serverMiddlewareFn } from "@server/serverSetup.js"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/serverSetup.js
|
||||
import cors from 'cors'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
import config from '@wasp/config.js'
|
||||
|
||||
export const serverMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
// Example of adding an extra domains to CORS.
|
||||
middlewareConfig.set('cors', cors({ origin: [config.frontendUrl, 'https://example1.com', 'https://example2.com'] }))
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Customize `api`-specific middleware
|
||||
|
||||
If you would like to modify the middleware for a single API, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
api webhookCallback {
|
||||
fn: import { webhookCallback } from "@server/apis.js",
|
||||
middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js",
|
||||
httpRoute: (POST, "/webhook/callback"),
|
||||
auth: false
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
import express from 'express'
|
||||
import { WebhookCallback } from '@wasp/apis/types'
|
||||
import { MiddlewareConfigFn } from '@wasp/middleware'
|
||||
|
||||
export const webhookCallback: WebhookCallback = (req, res, _context) => {
|
||||
res.json({ msg: req.body.length })
|
||||
}
|
||||
|
||||
export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
console.log('webhookCallbackMiddlewareFn: Swap express.json for express.raw')
|
||||
|
||||
middlewareConfig.delete('express.json')
|
||||
middlewareConfig.set('express.raw', express.raw({ type: '*/*' }))
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
:::note
|
||||
This gets installed on a per-method basis. Behind the scenes, this results in something like:
|
||||
|
||||
```js
|
||||
router.post('/webhook/callback', webhookCallbackMiddleware, ...)
|
||||
```
|
||||
:::
|
||||
|
||||
## 3. Customize per-path middleware
|
||||
|
||||
If you would like to modify the middleware for all API routes under some common path, you can do something like:
|
||||
|
||||
```c title=todoApp.wasp
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
```ts title=src/server/apis.ts
|
||||
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
|
||||
const customMiddleware : express.RequestHandler = (_req, _res, next) => {
|
||||
console.log('fooBarNamespaceMiddlewareFn: custom middleware')
|
||||
next()
|
||||
}
|
||||
|
||||
middlewareConfig.set('custom.middleware', customMiddleware)
|
||||
|
||||
return middlewareConfig
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This gets installed at the router level for the path. Behind the scenes, this results in something like:
|
||||
|
||||
```js
|
||||
router.use('/foo/bar', fooBarNamespaceMiddleware)
|
||||
```
|
||||
:::
|
@ -664,6 +664,7 @@ You can easily do this with the `api` declaration, which supports the following
|
||||
- `entities: [Entity]` (optional) - A list of entities you wish to use inside your API.
|
||||
We'll leave this option aside for now. You can read more about it [here](#using-entities-in-apis).
|
||||
- `auth: bool` (optional) - If auth is enabled, this will default to `true` and provide a `context.user` object. If you do not wish to attempt to parse the JWT in the Authorization Header, you may set this to `false`.
|
||||
- `middlewareConfigFn: ServerImport` (optional) - The import statement to an Express middleware config function for this API. See [the guide here](/docs/guides/middleware-customization#2-customize-api-specific-middleware).
|
||||
|
||||
Wasp APIs and their implementations don't need to (but can) have the same name. With that in mind, this is how you might declare the API that uses the implementations from the previous step:
|
||||
```c title="pages/main.wasp"
|
||||
@ -725,6 +726,19 @@ export const fooBar : FooBar = (req, res, context) => {
|
||||
|
||||
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
||||
|
||||
### `apiNamespace`
|
||||
|
||||
An `apiNamespace` is a simple declaration used to apply some `middlewareConfigFn` to all APIs under some specific path. For example:
|
||||
|
||||
```c title="main.wasp"
|
||||
apiNamespace fooBar {
|
||||
middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js",
|
||||
path: "/foo/bar"
|
||||
}
|
||||
```
|
||||
|
||||
For more information about middleware configuration, please see: [Middleware Configuration](/docs/guides/middleware-customization)
|
||||
|
||||
## Jobs
|
||||
|
||||
If you have server tasks that you do not want to handle as part of the normal request-response cycle, Wasp allows you to make that function a `job` and it will gain some "superpowers." Jobs will:
|
||||
@ -1863,6 +1877,10 @@ app MyApp {
|
||||
|
||||
`app.server` is a dictionary with following fields:
|
||||
|
||||
#### `middlewareConfigFn: ServerImport` (optional)
|
||||
|
||||
The import statement to an Express middleware config function. This is a global modification affecting all operations and APIs. See [the guide here](/docs/guides/middleware-customization#1-customize-global-middleware).
|
||||
|
||||
#### `setupFn: ServerImport` (optional)
|
||||
|
||||
`setupFn` declares a JS function that will be executed on server start. This function is expected to be async and will be awaited before the server starts accepting any requests.
|
||||
|
@ -67,6 +67,7 @@ While fundamental types are here to be basic building blocks of a language, and
|
||||
- Declaration types
|
||||
- **action**
|
||||
- **api**
|
||||
- **apiNamespace**
|
||||
- **app**
|
||||
- **entity**
|
||||
- **job**
|
||||
|
@ -61,6 +61,7 @@ module.exports = {
|
||||
"typescript",
|
||||
"guides/testing",
|
||||
"guides/sending-emails",
|
||||
"guides/middleware-customization",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user