Adds middleware customization (#1092)

This commit is contained in:
Shayne Czyzewski 2023-04-24 12:16:54 -04:00 committed by GitHub
parent 6742c5d286
commit 013a13dee2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 906 additions and 175 deletions

View File

@ -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

View File

@ -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) }

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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(
(

View File

@ -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 =}

View File

@ -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",
"}"
]

View File

@ -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

View File

@ -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"
],
[
[

View File

@ -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) }

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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

View File

@ -1,7 +1,7 @@
app waspBuild {
db: { system: PostgreSQL },
wasp: {
version: "^0.10.2"
version: "^0.10.3"
},
title: "waspBuild"
}

View File

@ -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

View File

@ -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"
],
[
[

View File

@ -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) }

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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

View File

@ -1,6 +1,6 @@
app waspCompile {
wasp: {
version: "^0.10.2"
version: "^0.10.3"
},
title: "waspCompile"
}

View File

@ -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

View File

@ -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"
],
[
[

View File

@ -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) }

View File

@ -0,0 +1,5 @@
import { MiddlewareConfigFn } from '../middleware'
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
return middlewareConfig
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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)
}
)
)

View File

@ -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

View File

@ -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"
}

View File

@ -0,0 +1,5 @@
import { MiddlewareConfigFn } from '@wasp/middleware'
export const fooBarNamespaceMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => {
return middlewareConfig
}

View File

@ -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
}

View File

@ -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

View File

@ -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"
],
[
[

View File

@ -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) }

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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

View File

@ -1,7 +1,7 @@
app waspJob {
db: { system: PostgreSQL },
wasp: {
version: "^0.10.2"
version: "^0.10.3"
},
title: "waspJob"
}

View File

@ -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

View File

@ -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"
],
[
[

View File

@ -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) }

View File

@ -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())
}

View File

@ -0,0 +1 @@
export * from './globalMiddleware.js'

View File

@ -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

View File

@ -1,6 +1,6 @@
app waspMigrate {
wasp: {
version: "^0.10.2"
version: "^0.10.3"
},
title: "waspMigrate"
}

View File

@ -1,6 +1,6 @@
app waspNew {
wasp: {
version: "^0.10.2"
version: "^0.10.3"
},
title: "waspNew"
}

View File

@ -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
}

View File

@ -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

View File

@ -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],

View File

@ -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 -}

View File

@ -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

View File

@ -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)

View 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

View File

@ -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)

View File

@ -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.

View File

@ -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
]

View File

@ -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 =

View File

@ -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 ->

View File

@ -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

View File

@ -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

View 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)
```
:::

View File

@ -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.

View File

@ -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**

View File

@ -61,6 +61,7 @@ module.exports = {
"typescript",
"guides/testing",
"guides/sending-emails",
"guides/middleware-customization",
],
},
{