sleepy-serv 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -24
- package/package.json +1 -1
- package/src/index.js +26 -14
- package/src/middleware.js +19 -4
package/README.md
CHANGED
|
@@ -73,19 +73,23 @@ The route's logic is implemented in the route definition files. These must `expo
|
|
|
73
73
|
Here's an example of a simple handler function:
|
|
74
74
|
|
|
75
75
|
```js
|
|
76
|
-
export default function (req, res) {
|
|
76
|
+
export default function (req, res, next) {
|
|
77
77
|
console.log('res:', res)
|
|
78
|
+
console.log('next:', next) // null for route handlers
|
|
78
79
|
|
|
79
80
|
return new Response('Hello world')
|
|
80
81
|
}
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
These functions take
|
|
84
|
+
These functions take three parameters:
|
|
84
85
|
- `req`: a `BunRequest` object
|
|
85
86
|
- `res`: a results object
|
|
87
|
+
- `next`: a function to call the next middleware in the chain (or `null` for the final handler)
|
|
86
88
|
|
|
87
89
|
The `res` parameter might seem familiar if coming from ExpressJS, but this parameter has a different purpose. The `res` parameter is meant to act as a dedicated, persistent object that middleware can write their results to. This way, the `req` object stays as a lean `BunRequest`.
|
|
88
90
|
|
|
91
|
+
The `next` parameter is a function that allows middleware to pass control to the next function in the middleware chain. For route handlers (the final function in the chain), `next` will be `null`.
|
|
92
|
+
|
|
89
93
|
These handlers can also be `async` functions and `sleepy-serv` will wait for them to finish before moving on.
|
|
90
94
|
|
|
91
95
|
### Middleware
|
|
@@ -94,10 +98,11 @@ As mentioned earlier, method definition files can also export an array of functi
|
|
|
94
98
|
|
|
95
99
|
```js
|
|
96
100
|
export default [
|
|
97
|
-
async (req, res) => {
|
|
101
|
+
async (req, res, next) => {
|
|
98
102
|
res.body = await req.json()
|
|
103
|
+
return next() /* call the next middleware */
|
|
99
104
|
},
|
|
100
|
-
(req, res) => {
|
|
105
|
+
(req, res) => { /* notice that the last function doesn't take `next` */
|
|
101
106
|
console.log('JSON body:', res.body)
|
|
102
107
|
|
|
103
108
|
return new Response('Hello world')
|
|
@@ -105,9 +110,58 @@ export default [
|
|
|
105
110
|
]
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
This is useful if you want to break common logic up into reusable functions. The functions in the array are called in-order, and
|
|
113
|
+
This is useful if you want to break common logic up into reusable functions. The functions in the array are called in-order, and each receives three parameters: `req`, `res`, and `next`.
|
|
114
|
+
|
|
115
|
+
#### Middleware Chain Behavior
|
|
116
|
+
|
|
117
|
+
Each middleware function must do one of the following:
|
|
118
|
+
- **Call `next()`**: Continue to the next middleware in the chain by returning `next()`
|
|
119
|
+
- **Return a Response**: End the chain (early) by returning a `BunResponse` object
|
|
120
|
+
- **Throw an error**: Stop the chain and trigger error handling
|
|
121
|
+
|
|
122
|
+
The `next` parameter will be `null` for the final function in the chain (the route handler), indicating there are no more middleware functions to call.
|
|
123
|
+
|
|
124
|
+
#### Example with Explicit `next()` Calls
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
export default [
|
|
128
|
+
(req, res, next) => {
|
|
129
|
+
console.log('1st middleware')
|
|
130
|
+
|
|
131
|
+
if (req.headers.get('authorization')) {
|
|
132
|
+
return next() // continue to next middleware
|
|
133
|
+
} else {
|
|
134
|
+
return new Response('Unauthorized', { status: 401 }) // end chain early
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
(req, res, next) => {
|
|
138
|
+
console.log('2nd middleware')
|
|
139
|
+
|
|
140
|
+
res.user = {
|
|
141
|
+
id: 123,
|
|
142
|
+
name: 'John',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return next() // continue to route handler
|
|
146
|
+
},
|
|
147
|
+
(req, res) => {
|
|
148
|
+
console.log('Final, "route" handler')
|
|
149
|
+
|
|
150
|
+
return new Response(`Hello ${res.user.name}`)
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Middleware Execution Order
|
|
156
|
+
|
|
157
|
+
Middleware functions are executed in the following order:
|
|
158
|
+
1. **App-level middleware** (defined in `createApp()` options)
|
|
159
|
+
2. **Directory-level middleware** (from `meta.js` files, from root to leaf)
|
|
160
|
+
3. **Route-level middleware** (defined in the route file array)
|
|
109
161
|
|
|
110
|
-
|
|
162
|
+
Each middleware function receives the same `req` and `res` objects, allowing data to be passed between middleware functions through the `res` object.
|
|
163
|
+
|
|
164
|
+
#### Breaking the Middleware Chain - Responses
|
|
111
165
|
|
|
112
166
|
You might want to break the middleware chain with a response early.
|
|
113
167
|
|
|
@@ -115,27 +169,29 @@ Here's an example:
|
|
|
115
169
|
|
|
116
170
|
```js
|
|
117
171
|
export default [
|
|
118
|
-
req => {
|
|
172
|
+
(req, res, next) => {
|
|
119
173
|
if (req.params.userId === '123') {
|
|
120
174
|
return new Response('Returned early')
|
|
175
|
+
} else {
|
|
176
|
+
return next() // continue to next middleware
|
|
121
177
|
}
|
|
122
178
|
},
|
|
123
|
-
req => {
|
|
179
|
+
(req, res) => {
|
|
124
180
|
return new Response('End of chain')
|
|
125
181
|
},
|
|
126
182
|
]
|
|
127
183
|
```
|
|
128
184
|
|
|
129
|
-
|
|
185
|
+
In the case above, the 1st middleware function will return a `Response` object if the route param's `userId` is equal to a specific value. In those cases, the last function in the middleware chain will not execute for that request. If the condition is not met, `next()` is called to continue to the next middleware.
|
|
130
186
|
|
|
131
|
-
|
|
187
|
+
#### Breaking the Middleware Chain - Errors
|
|
132
188
|
|
|
133
189
|
It's also common to throw errors for things like request validation or when a desired resource is not found. `sleepy-serv` has defined custom `Error` types for every type of 4xx and 5xx error:
|
|
134
190
|
|
|
135
191
|
```js
|
|
136
192
|
import { NotFoundError } from 'sleepy-serv'
|
|
137
193
|
|
|
138
|
-
export default async function (req) {
|
|
194
|
+
export default async function (req, res) {
|
|
139
195
|
const users = await sql`
|
|
140
196
|
SELECT * FROM Users
|
|
141
197
|
WHERE userId=${req.params.userId}
|
|
@@ -156,7 +212,7 @@ If the user is found in the database, then the request will return with a succes
|
|
|
156
212
|
Throwing generic errors also works too:
|
|
157
213
|
|
|
158
214
|
```js
|
|
159
|
-
export default async function (req) {
|
|
215
|
+
export default async function (req, res) {
|
|
160
216
|
throw new Error('A problem occurred')
|
|
161
217
|
}
|
|
162
218
|
```
|
|
@@ -167,7 +223,7 @@ export default async function (req) {
|
|
|
167
223
|
|
|
168
224
|
It's also possible for resource directories to contain a `meta.js` file. These files can export various things that have some sort semantic relationship to the part of the route that they're defined in.
|
|
169
225
|
|
|
170
|
-
|
|
226
|
+
#### Directory-Level Middleware
|
|
171
227
|
|
|
172
228
|
`meta.js` files can also export an array of middleware functions:
|
|
173
229
|
|
|
@@ -175,7 +231,10 @@ It's also possible for resource directories to contain a `meta.js` file. These f
|
|
|
175
231
|
// meta.js
|
|
176
232
|
|
|
177
233
|
export const middleware = [
|
|
178
|
-
req => {
|
|
234
|
+
(req, res, next) => {
|
|
235
|
+
/* do middleware things */
|
|
236
|
+
return next() // Continue to next middleware
|
|
237
|
+
},
|
|
179
238
|
]
|
|
180
239
|
```
|
|
181
240
|
|
|
@@ -223,25 +282,69 @@ The middleware defined in `/api/users/:userId/meta.js` will be applied the follo
|
|
|
223
282
|
- `DELETE /users/:userId`
|
|
224
283
|
- `PUT /users/:userId`
|
|
225
284
|
|
|
226
|
-
###
|
|
285
|
+
### Meta Files
|
|
227
286
|
|
|
228
|
-
At the time of this writing, `meta.js` only exports middleware functions.
|
|
287
|
+
At the time of this writing, `meta.js` only exports middleware functions, but it will eventually be able to provide additional configuration for the endpoint definitions within its scope.
|
|
229
288
|
|
|
230
|
-
##
|
|
289
|
+
## Built-In Middleware
|
|
231
290
|
|
|
232
291
|
`sleepy-serv` also comes built-in with a few useful middleware functions that are commonly used.
|
|
233
292
|
|
|
234
|
-
### parseJson(req,
|
|
293
|
+
### parseJson(req, res, next)
|
|
294
|
+
|
|
295
|
+
This middleware parses the request's `body` property as a JSON string, and then stores the results in `res.body`. It calls `next()` if parsing succeeds, and throws a `BadRequestError` if parsing fails. Additional body parsers can be written in the future to accommodate other body encoding schemes (such as XML or protobuf).
|
|
235
296
|
|
|
236
|
-
|
|
297
|
+
Example usage:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
import { middleware } from 'sleepy-serv'
|
|
301
|
+
|
|
302
|
+
export default [
|
|
303
|
+
middleware.parseJson,
|
|
304
|
+
(req, res) => {
|
|
305
|
+
console.log('Parsed JSON:', res.body)
|
|
306
|
+
|
|
307
|
+
return new Response('JSON received')
|
|
308
|
+
},
|
|
309
|
+
]
|
|
310
|
+
```
|
|
237
311
|
|
|
238
312
|
### validateSchema(schemas)
|
|
239
313
|
|
|
240
|
-
This function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- `
|
|
244
|
-
- `
|
|
314
|
+
This function returns a middleware function that validates request data against provided schemas. The returned middleware function has the signature `(req, res, next)` and automatically calls `next()` after successful validation.
|
|
315
|
+
|
|
316
|
+
The `schemas` object can contain these optional properties:
|
|
317
|
+
- `headers`: takes a string formatter schema to evaluate `req.headers`
|
|
318
|
+
- `params`: takes a string formatter schema to evaluate `req.params`
|
|
319
|
+
- `query`: takes a string formatter schema to evaluate `req.query`
|
|
320
|
+
- `body`: takes a JSON validation schema to evaluate `res.body`
|
|
321
|
+
|
|
322
|
+
Example usage:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
import { middleware } from 'sleepy-serv'
|
|
326
|
+
|
|
327
|
+
export default [
|
|
328
|
+
middleware.parseJson,
|
|
329
|
+
middleware.validateSchema({
|
|
330
|
+
params: {
|
|
331
|
+
userId: { type: 'format', value: 'uuid' }
|
|
332
|
+
},
|
|
333
|
+
body: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: {
|
|
336
|
+
name: { type: 'string' },
|
|
337
|
+
email: { type: 'string', format: 'email' }
|
|
338
|
+
},
|
|
339
|
+
required: ['name', 'email']
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
(req, res, next) => {
|
|
343
|
+
// Validation passed, process the request
|
|
344
|
+
return new Response('Data is valid')
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
```
|
|
245
348
|
|
|
246
349
|
Here's an example of the schema in action:
|
|
247
350
|
|
|
@@ -295,6 +398,26 @@ Calling `setValidationFormats()` extends the possible values that can be passed
|
|
|
295
398
|
|
|
296
399
|
## `createApp()` Options
|
|
297
400
|
|
|
401
|
+
### `middleware`
|
|
402
|
+
|
|
403
|
+
You can define app-level middleware that will be applied to all routes:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
import { middleware } from 'sleepy-serv'
|
|
407
|
+
|
|
408
|
+
const app = await createApp(PORT, import.meta.dirname, {
|
|
409
|
+
middleware: [
|
|
410
|
+
middleware.parseJson,
|
|
411
|
+
(req, res, next) => {
|
|
412
|
+
console.log(`${req.method} ${req.url}`)
|
|
413
|
+
return next()
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
App-level middleware is executed before any directory-level or route-level middleware.
|
|
420
|
+
|
|
298
421
|
### `hostname`
|
|
299
422
|
|
|
300
423
|
The hostname can be customized like so:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -45,17 +45,18 @@ function methodNotAllowedHandler (_req) {
|
|
|
45
45
|
throw new MethodNotAllowedError()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/* TODO: add whitelist support */
|
|
48
49
|
function validateDirectoryIllegalFiles (targetPath, filenames) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Directory contains illegal files:
|
|
56
|
-
${targetPath}
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// const hasInvalidFiles = filenames.some(filename =>
|
|
51
|
+
// !ALLOWED_FILES_ALL.includes(filename)
|
|
52
|
+
// )
|
|
53
|
+
|
|
54
|
+
// if (hasInvalidFiles) {
|
|
55
|
+
// throw new TypeError(`
|
|
56
|
+
// Directory contains illegal files:
|
|
57
|
+
// ${targetPath}
|
|
58
|
+
// `.trim())
|
|
59
|
+
// }
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function validateLeafDirectory (targetPath, filenames, entries) {
|
|
@@ -191,13 +192,24 @@ ${route.modulePath}
|
|
|
191
192
|
|
|
192
193
|
req.query = query ? querystring.parse(query) : {}
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
const
|
|
195
|
+
const executeMiddleware = async (index) => {
|
|
196
|
+
const currentMiddleware = middlewareChain[index]
|
|
197
|
+
const isLastMiddleware = index === middlewareChain.length - 1
|
|
198
|
+
|
|
199
|
+
const next = !isLastMiddleware ?
|
|
200
|
+
() => executeMiddleware(index + 1)
|
|
201
|
+
: null
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
const result = await currentMiddleware(req, res, next)
|
|
204
|
+
|
|
205
|
+
if (result instanceof Response) {
|
|
206
|
+
return result
|
|
207
|
+
} else {
|
|
208
|
+
throw new TypeError('Handler does not return a Response object')
|
|
199
209
|
}
|
|
200
210
|
}
|
|
211
|
+
|
|
212
|
+
return executeMiddleware(0)
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
return {
|
package/src/middleware.js
CHANGED
|
@@ -2,6 +2,7 @@ import Ajv from 'ajv'
|
|
|
2
2
|
import addFormats from 'ajv-formats'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
BadRequestError,
|
|
5
6
|
UnsupportedMediaTypeError,
|
|
6
7
|
UnprocessableContentError,
|
|
7
8
|
} from './errors'
|
|
@@ -46,14 +47,26 @@ function buildFormatterSchema (schema) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export async function parseJson (req, res) {
|
|
50
|
+
export async function parseJson (req, res, next) {
|
|
50
51
|
const contentType = req.headers.get('content-type')
|
|
51
52
|
|
|
52
|
-
if (
|
|
53
|
+
if (!contentType) {
|
|
54
|
+
return next()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (contentType !== 'application/json') {
|
|
53
58
|
throw new UnsupportedMediaTypeError('content-type')
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
try {
|
|
62
|
+
res.body = await req.json()
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(err)
|
|
65
|
+
|
|
66
|
+
throw new BadRequestError('Body is invalid JSON')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return next()
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export function setValidationFormats (formats) {
|
|
@@ -61,7 +74,7 @@ export function setValidationFormats (formats) {
|
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
export function validateSchema (schemas) {
|
|
64
|
-
return function (req, res) {
|
|
77
|
+
return function (req, res, next) {
|
|
65
78
|
const formattedSchemas = {
|
|
66
79
|
headers: buildFormatterSchema(schemas.headers),
|
|
67
80
|
params: buildFormatterSchema(schemas.params),
|
|
@@ -97,5 +110,7 @@ export function validateSchema (schemas) {
|
|
|
97
110
|
if (errors.length > 0) {
|
|
98
111
|
throw new UnprocessableContentError(errors)
|
|
99
112
|
}
|
|
113
|
+
|
|
114
|
+
return next()
|
|
100
115
|
}
|
|
101
116
|
}
|