@wooksjs/event-http 0.1.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 ADDED
@@ -0,0 +1,583 @@
1
+ # @wooksjs/event-http
2
+
3
+ **!!! This is work-in-progress library, breaking changes are expected !!!**
4
+
5
+ <p align="center">
6
+ <img src="../../logo.png" width="128px"><br>
7
+ <a href="https://github.com/prostojs/wooks/blob/main/LICENSE">
8
+ <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" />
9
+ </a>
10
+ </p>
11
+
12
+ As a part of `wooks` event processing framework, `@wooksjs/event-http` implements http events and provides composables that let you:
13
+ - parse urls search params
14
+ - parse cookies
15
+ - parse request body (json, url-encoded, form, ...)
16
+ - serve files
17
+
18
+ ### The main ideas behind composable functions are:
19
+
20
+ 1. Never mutate request object (`req`). Accumulate a request context in a separate object(s) instead (`wooks store`);
21
+ 2. Never parse anything (cookies, body) before it is really requested by the request handler;
22
+ 3. Get rid of complex predefined data objects containing everything (cookies, headers, body, parsed body etc.) and use composable functions (hooks) instead;
23
+ 4. Get rid of tons of dependencies (middlewares) and implement everything that is needed for web app in a simple way.
24
+
25
+ ### Official Wooks HTTP composables:
26
+
27
+ - [@wooksjs/http-static](https://github.com/wooksjs/wooksjs/tree/main/packages/http-static) - to serve static files
28
+ - [@wooksjs/http-proxy](https://github.com/wooksjs/wooksjs/tree/main/packages/http-proxy) - to proxy requests
29
+
30
+ ## Install
31
+
32
+ `npm install wooks @wooksjs/event-http`
33
+
34
+ ## Quick Start
35
+
36
+ ```js
37
+ import { Wooks, useRouteParams } from 'wooks'
38
+ import { WooksHttp } from '@wooksjs/event-http'
39
+
40
+ const app = new Wooks()
41
+
42
+ app.on('GET', 'hello/:name', () => `Hello ${ useRouteParams().get('name') }!`)
43
+
44
+ app.subscribe(new WooksHttp(3000, () => {
45
+ console.log('Wooks Server is up on port 3000')
46
+ }))
47
+ ```
48
+
49
+ ## Shortcuts
50
+
51
+ You can use `get`, `post`, ... methods directly with `shortcuts` feature:
52
+
53
+ ```js
54
+ import { useRouteParams, Wooks } from 'wooks'
55
+ import { httpShortcuts, WooksHttp } from '@wooksjs/event-http'
56
+
57
+ const app = new Wooks().shortcuts(httpShortcuts)
58
+
59
+ app.get('hello/:name', () => `Hello ${ useRouteParams().get('name') }!`)
60
+
61
+ app.subscribe(new WooksHttp(3000))
62
+ ```
63
+
64
+ ## Quick Navigation
65
+ - [User Documentation](./README.md#User-Documentation)
66
+ - [URL Parameters](./README.md#)
67
+ - [Query Parameters](./README.md#Query-Parameters)
68
+ ---
69
+ - [Request](./README.md#Request)
70
+ - [Request Method and Headers](./README.md#Request-Method-and-Headers)
71
+ - [Request Cookies](./README.md#Request-Cookies)
72
+ - [Request Authorization](./README.md#Request-Authorization)
73
+ - [Request Body Parser](./README.md#Request-Body-Parser)
74
+ ---
75
+ - [Response](./README.md#Response)
76
+ - [Response Headers](./README.md#Response-Headers)
77
+ - [Response Cookies](./README.md#Response-Cookies)
78
+ - [Response Status](./README.md#Response-Status)
79
+ - [Cache-Control](./README.md#Cache-Control)
80
+ - [Proxy Requests](./README.md#Proxy-Requests)
81
+ - [Serve File](./README.md#Serve-File)
82
+ ---
83
+ - [Create you own hooks](./README.md#Create-you-own-hooks)
84
+ ---
85
+ - [Adapter Documentation](./README.md#Adapter-Documentation)
86
+ - [Create a new wooks context](./README.md#Create-a-new-wooks-context)
87
+ - [Create a responder](./README.md#Create-a-responder)
88
+ - [Restore Context](./README.md#Restore-Context)
89
+
90
+ ## User Documentation
91
+
92
+ ### URL Parameters
93
+ To get access to URL parameters use composable function `useRouteParams`
94
+
95
+ ```js
96
+ import { useRouteParams } from 'wooks'
97
+ app.get('parametric/:param1/:param2/...', () => {
98
+ const { params, get } = useRouteParams()
99
+ // presume we had a request on `/parametric/value1/value2`
100
+ console.log('param1=' + get('param1'))
101
+ // prints "param1=value1"
102
+ console.log('param2=' + get('param2'))
103
+ // prints "param2=value2"
104
+ console.log(params)
105
+ // prints {
106
+ // param1: "value1",
107
+ // param2: "value2"
108
+ // }
109
+ })
110
+ ```
111
+
112
+ ### Query Parameters
113
+ To get access to Query parameters use composable function `useSearchParams`
114
+
115
+ ```js
116
+ import { useSearchParams } from '@wooksjs/event-http'
117
+ app.get('with-query', () => {
118
+ const { jsonSearchParams, urlSearchParams } = useSearchParams()
119
+ // presume we had a request on `/with-query?param1=abc&param2=cde`
120
+ console.log('param1=' + urlSearchParams('param1'))
121
+ // prints "param1=abc"
122
+ console.log('param2=' + urlSearchParams('param2'))
123
+ // prints "param1=cde"
124
+ console.log(jsonSearchParams)
125
+ // prints {
126
+ // param1: "abc",
127
+ // param2: "cde"
128
+ // }
129
+ })
130
+ ```
131
+
132
+ ### Request
133
+ To get a reference to the raw request instance use composable function `useRequest`
134
+
135
+ You probably don't need a `rawRequest` unless you are developing some new feature. All the base use-cases covered with other composable functions.
136
+
137
+ ```js
138
+ import { useRequest } from '@wooksjs/event-http'
139
+ app.get('test', () => {
140
+ const { rawRequest } = useRequest()
141
+ })
142
+ ```
143
+
144
+ ### Request Method and Headers
145
+ `useRequest` provides some more shortcuts for useful data
146
+
147
+ ```js
148
+ import { useRequest } from '@wooksjs/event-http'
149
+ app.get('test', async () => {
150
+ const {
151
+ url, // request url (string)
152
+ method, // request method (string)
153
+ headers, // request headers (object)
154
+ rawBody, // request body ((): Promise<Buffer>)
155
+ } = useRequest()
156
+
157
+ const body = await rawBody() // body as a Buffer
158
+ })
159
+ ```
160
+
161
+ ### Request Cookies
162
+ Cookies are not parsed unless requested. Composable function `useCookies` provides cookie getter and raw cookies string.
163
+
164
+ ```js
165
+ import { useCookies } from '@wooksjs/event-http'
166
+ app.get('test', async () => {
167
+ const {
168
+ rawCookies, // "cookie" from headers (string | undefined)
169
+ getCookie, // cookie getter ((name): string | null)
170
+ } = useCookies()
171
+
172
+ console.log(getCookie('session'))
173
+ // prints the value of the cookie with the name "session"
174
+ })
175
+ ```
176
+
177
+ ### Request Authorization
178
+ `useAuthorization` function provides useful helpers for auth-headers:
179
+
180
+ ```js
181
+ import { useAuthorization } from '@wooksjs/event-http'
182
+ app.get('test', async () => {
183
+ const {
184
+ authorization, // the raw value of "authorization" header : string
185
+ authType, // the auth type (Bearer/Basic) : string
186
+ authRawCredentials, // the auth credentials that follow auth type : string
187
+ isBasic, // true if authType === 'Basic' : () => boolean
188
+ isBearer, // true if authType === 'Bearer' : () => boolean
189
+ basicCredentials, // parsed basic auth credentials : () => { username: string, password: string }
190
+ } = useAuthorization()
191
+
192
+ if (isBasic()) {
193
+ const { username, password } = basicCredentials()
194
+ console.log({ username, password })
195
+ } else if (isBearer()) {
196
+ const token = authRawCredentials
197
+ console.log({ token })
198
+ } else {
199
+ // unknown or empty authorization header
200
+ }
201
+ })
202
+ ```
203
+
204
+ ### Request Body Parser
205
+ [More details here](https://github.com/wooksjs/body#readme)
206
+
207
+ ### Response
208
+ The easiest way to respond to the request is to return some value from handler function like this:
209
+ ```js
210
+ app.get('string_response', () => {
211
+ return 'hello world!'
212
+ // responds with:
213
+ // 200
214
+ // Content-Length: ...
215
+ // Content-Type: text/plain
216
+ // hello world!
217
+ })
218
+ ```
219
+
220
+ Whatever is returned from the handler is the response. `Content-Type` and `Content-Length` headers will be calculated accordingly.
221
+
222
+ If a handler returns a json object, it will be stringified and the header `Content-Type` will be set to `application/json` automatically:
223
+ ```js
224
+ app.get('json_response', () => {
225
+ return { value: 'hello world!' }
226
+ // responds with:
227
+ // 200
228
+ // Content-Length: ...
229
+ // Content-Type: application/json
230
+ // { "value": "hello world!" }
231
+ })
232
+ ```
233
+
234
+ **Supported response types:**
235
+ 1. string (text/plain, text/html, application/xml - depending on the content)
236
+ 2. object/array (application/json)
237
+ 3. boolean (text/plain)
238
+ 4. readable stream (you must specify `Content-Type` and `Content-Length` headers yourself)
239
+ 5. fetch (native) response (streaming body to client response)
240
+
241
+ **Raw Response**: When it is needed to take the full control of the response, use composable function `useResponse`
242
+
243
+ When you get a raw response instance you take away the control of the response on yourself. The framework will not process the output of the handler in this case.
244
+
245
+ An example of using raw response instance:
246
+ ```js
247
+ import { useResponse } from '@wooksjs/event-http'
248
+ app.get('test', () => {
249
+ const { rawResponse } = useResponse()
250
+ const res = rawResponse()
251
+ res.writeHead(200, {})
252
+ res.end('ok')
253
+ })
254
+ ```
255
+
256
+ If you don't want to take away a responsibility for the response but still need a raw response instance you can use `{ passthrough: true }` as an argument.
257
+ The next example does the same thing as the previous example using `passthrough` options:
258
+
259
+ ```js
260
+ import { useResponse } from '@wooksjs/event-http'
261
+ app.get('test', () => {
262
+ const { rawResponse } = useResponse()
263
+ const res = rawResponse({ passthrough: true })
264
+ return 'ok'
265
+ })
266
+ ```
267
+
268
+ ### Response Headers
269
+ A function `useSetHeaders` provides variety of response headers helpers:
270
+
271
+ ```js
272
+ import { useSetHeaders, contentTypes } from '@wooksjs/event-http'
273
+ app.get('test', async () => {
274
+ const {
275
+ setHeader, //sets header: (name: string, value: string | number) => void;
276
+ removeHeader, //removes header: (name: string) => void;
277
+ setContentType, //sets "Content-Type": (value: string) => void;
278
+ headers, //Object with response headers: Record<string, string>;
279
+ enableCors, //sets "Access-Control-Allow-Origin": (origin?: string) => void;
280
+ } = useSetHeaders()
281
+
282
+ setContentType(contentTypes.application.json)
283
+ setHeader('server', 'myServer v1.0')
284
+ enableCors()
285
+ return '{ "value": "OK" }'
286
+ })
287
+ ```
288
+
289
+ Another hook for set header
290
+
291
+ ```js
292
+ import { useSetHeader } from '@wooksjs/event-http'
293
+ app.get('test', async () => {
294
+ const server = useSetHeader('server')
295
+ server.value = 'myServer v1.0'
296
+ })
297
+ ```
298
+
299
+ ### Response Cookies
300
+ A function `useSetCookies` provides variety of set-cookie helpers:
301
+
302
+ ```js
303
+ import { useSetCookies } from '@wooksjs/event-http'
304
+ app.get('test', async () => {
305
+ const {
306
+ setCookie, // sets cookie : (name: string, value: string, attrs?) => void;
307
+ removeCookie, // removes cookie from setlist : (name: string) => void;
308
+ clearCookies, // removes all the cookies from setlist : () => void;
309
+ cookies, // returns a value of Set-Cookie header: () => string[];
310
+ } = useSetCookies()
311
+
312
+ setCookie('session', 'value', {
313
+ expires: '2029-01-01', // Date | string | number;
314
+ maxAge: '1h', // number | TProstoTimeMultiString;
315
+ domain: 'my-domain', // string;
316
+ path: '/home', // string;
317
+ secure: true, // boolean;
318
+ httpOnly: false, // boolean;
319
+ sameSite: true, // boolean | 'Lax' | 'None' | 'Strict';
320
+ })
321
+ })
322
+ ```
323
+
324
+ Another hook for set-cookie
325
+
326
+ ```js
327
+ import { useSetCookie } from '@wooksjs/event-http'
328
+ app.get('test', async () => {
329
+ const session = useSetCookie('session')
330
+ session.value = 'value'
331
+ session.attrs = {
332
+ expires: '2029-01-01', // Date | string | number;
333
+ maxAge: '1h', // number | TProstoTimeMultiString;
334
+ domain: 'my-domain', // string;
335
+ path: '/home', // string;
336
+ secure: true, // boolean;
337
+ httpOnly: false, // boolean;
338
+ sameSite: true, // boolean | 'Lax' | 'None' | 'Strict';
339
+ }
340
+ })
341
+ ```
342
+
343
+ ### Response Status
344
+ It's possible to control the response status via `status` function that is available in `useResponse()`
345
+
346
+ ```js
347
+ import { useResponse } from '@wooksjs/event-http'
348
+ app.get('test', async () => {
349
+ const { status } = useResponse()
350
+
351
+ // use function calls:
352
+ status(201) // sets status 201 for the response
353
+
354
+ console.log(status()) // when called with no argument returns the status
355
+
356
+ // also possible to use value:
357
+ // status.value = 201
358
+ // console.log(status.value)
359
+
360
+ return 'response with status 201'
361
+ })
362
+ ```
363
+
364
+ ### Cache-Control
365
+ `useSetCacheControl` function provides helpers for headers responsible for cache control
366
+
367
+ ```js
368
+ import { useSetCacheControl } from '@wooksjs/event-http'
369
+ app.get('static/*', () => {
370
+ const {
371
+ setAge, // sets Age (v: number | TProstoTimeMultiString) => void
372
+ setExpires, // sets Expires (v: Date | string | number) => void
373
+ setPragmaNoCache, // sets Pragma: no-cache (v: boolean) => void
374
+ setCacheControl, // sets Cache-Control (data: TCacheControl) => void
375
+ } = useSetCacheControl()
376
+
377
+ setAge('2h 15m')
378
+ setExpires('2022-05-05')
379
+ setCacheControl({
380
+ mustRevalidate: true,
381
+ noCache: false,
382
+ noStore: false,
383
+ noTransform: true,
384
+ public: true,
385
+ private: 'field',
386
+ proxyRevalidate: true,
387
+ maxAge: '3h 30m 12s',
388
+ sMaxage: '2h 27m 54s',
389
+ })
390
+ })
391
+ ```
392
+
393
+ ### Proxy Requests
394
+ [More details here](https://github.com/wooksjs/proxy#readme)
395
+
396
+ ### Serve File
397
+ [More details here](https://github.com/wooksjs/serve-file#readme)
398
+
399
+ ## Create you own hooks
400
+
401
+ As an example we'll create a composable that resolves user profile
402
+
403
+ ```ts
404
+ import { useAuthorization, useHttpContext } from '@wooksjs/event-http'
405
+
406
+ interface TUser {
407
+ username: string
408
+ age: number
409
+ // ...
410
+ }
411
+
412
+ export function useUserProfile() {
413
+ // 1. get custom-typed context
414
+ const { store } = useHttpContext<{ user: TUser }>()
415
+ const user = store('user')
416
+
417
+ // 2. in this example will use basic credentials approach to get user name
418
+ const { basicCredentials } = useAuthorization()
419
+
420
+ // 3. get user name
421
+ const username = basicCredentials()?.username
422
+
423
+ // 4. user data async loader
424
+ async function userProfile() {
425
+ // first check if user data was already cached
426
+ // for this request
427
+ if (!user.value) {
428
+ // no user data cached yet, try to read user
429
+ // and return the result
430
+ user.value = await readUser()
431
+ }
432
+ // return user profile from cache
433
+ return user.value
434
+ }
435
+
436
+ // abstract readUser function
437
+ function readUser(): Promise<TUser> {
438
+ // return db.readUser(username)
439
+ }
440
+
441
+ return {
442
+ username, // we have user name syncronously
443
+ userProfile, // and userProfile as (() => Promise<TUser>)
444
+ }
445
+ }
446
+
447
+ // example of usage of our useUserProfile
448
+ app.get('/user', async () => {
449
+ const { username, userProfile } = useUserProfile()
450
+ console.log('username =', username)
451
+ const data = await userProfile()
452
+ return { user: data }
453
+ })
454
+ ```
455
+
456
+ Example of custom set header hook
457
+
458
+ ```ts
459
+ import { useSetHeaders } from '@wooksjs/event-http'
460
+ import { attachHook } from '@wooksjs/event-core'
461
+
462
+ function useHeaderHook(name: string) {
463
+ const { setHeader, headers } = useSetHeaders()
464
+
465
+ return attachHook({
466
+ name,
467
+ type: 'header',
468
+ }, {
469
+ get: () => headers()[name] as string,
470
+ set: (value: string | number) => setHeader(name, value),
471
+ })
472
+ }
473
+
474
+ // usage
475
+
476
+ app.get('/test', () => {
477
+ const myHeader = useHeaderHook('x-my-header')
478
+ myHeader.value = 'header value'
479
+ // *Please note that useSetHeader('x-my-header') will work similarly*
480
+ return 'ok'
481
+ })
482
+
483
+ // result:
484
+ // 200
485
+ // headers:
486
+ // x-my-header: header value
487
+ ```
488
+
489
+ ## Adapter Documentation
490
+
491
+ The next paragraph is for those who wants to create threir own adapter and get more control over the wooks.
492
+
493
+ ### Create a new wooks context
494
+
495
+ Wooks context is a special object that stores:
496
+ 1. **instance of request** - is used to get data from request
497
+ 2. **instance of response** - is used to send a response
498
+ 3. **parsed route params** - usually Web App frameworks put it to `req` (`req.params`)
499
+ 4. **cache store** - is used to cache alread computed values, e.g. parsed body, parsed cookies, ..., in order to avoid multiple parsing/computetaion of the same entities.
500
+
501
+ When request is generated by the client a new Wooks context must be created.
502
+
503
+ `const {restoreCtx, clearCtx} = createHttpContext({ req, res })`
504
+
505
+ ```ts
506
+ import { createHttpContext } from '@wooksjs/event-http'
507
+
508
+ function requestHandler(req, res) {
509
+ const {
510
+ // restoreCtx hook helps to restore wooks context
511
+ // after any async operatuon
512
+ restoreCtx,
513
+
514
+ // clearCtx hook helps to clear wooks context
515
+ // when the request is already processed
516
+ clearCtx,
517
+
518
+ // store hook to access wooks store of the current ctx
519
+ store,
520
+ } = createHttpContext({
521
+ // request instance
522
+ req,
523
+ // response instance
524
+ res,
525
+ })
526
+
527
+ // if req already contains parsed params (req.params)
528
+ // then those will be picked up automatically
529
+ // unless you overwrite it here
530
+ store('routeParams').value = { name: 'value' }
531
+ }
532
+
533
+ ```
534
+
535
+ ### Create a responder
536
+
537
+ Responder is a function that takes any output of the request handler and transforms it into http response, processing headers, cookies, formats etc.
538
+
539
+ `const {createResponse, respond} = createWooksResponder()`
540
+
541
+ ```ts
542
+ import { createWooksResponder, createHttpContext } from '@wooksjs/event-http'
543
+
544
+ const {createResponse, respond} = createWooksResponder(
545
+ /*renderer, // (optional) instance of TWooksResponseRenderer*/
546
+ /*errorRenderer, // (optional) instance of TWooksResponseRenderer*/
547
+ )
548
+
549
+ async function requestHandler(req, res) {
550
+ // 1. create wooks context
551
+ const {restoreCtx, clearCtx} = createHttpContext({ req, res })
552
+ // 2. process request based on routers/handlers and get the output
553
+ const response = await createHttpContext()
554
+ // 3. restore wooks context
555
+ restoreCtx()
556
+ // 4. respond
557
+ respond(response)
558
+ // 5. clear wooks context
559
+ clearCtx()
560
+ }
561
+
562
+ async function processHandlers() {
563
+ // routing, processing, handling, ...
564
+ }
565
+
566
+ ```
567
+
568
+ ### Restore Context
569
+
570
+ There is one more way to get `restoreCtx` hook besides the one you get when `createHttpContext`
571
+
572
+ `const { restoreCtx, clearCtx } = useHttpContext()`
573
+
574
+ ```ts
575
+ import { useHttpContext } from '@wooksjs/event-http'
576
+
577
+ async function someHandler() {
578
+ const { restoreCtx, clearCtx } = useHttpContext()
579
+ await ... // some async operations
580
+ restoreCtx()
581
+ // here the wooks context is back
582
+ }
583
+ ```