Skip to content

Commit 6e1b5d6

Browse files
authored
Merge pull request #148 from supercharge/header-bag-refactoring
HeaderBag extends InputBag refactoring
2 parents 247d102 + fd2f452 commit 6e1b5d6

File tree

13 files changed

+286
-234
lines changed

13 files changed

+286
-234
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## [4.0.0](https://github.com/supercharge/framework/compare/v3.20.4...v4.0.0) - 2023-xx-xx
44

55
### Added
6+
- `@supercharge/contracts`
7+
- add `HttpDefaultRequestHeaders` and `HttpDefaultRequestHeader` interfaces: these are strict contracts for HTTP headers allowing IntelliSense for individual headers. IntelliSense is not supported on Node.js’s `IncomingHttpHeaders` interface because it contains an index signature which opens the interfaces to basically anything … the newly added interfaces are strict for allowed keys
8+
- add `HttpRequestHeaders` and `HttpRequestHeader` interfaces: `HttpRequestHeaders` is an interface to be used by developers for augmentation to add custom, project-specific request headers. For example, this can be used to add headers for rate limiting
69
- `@supercharge/hashing`
710
- add `createHash` method: create a Node.js `Hash` instance for a given input
811
- add `md5` method: create a Node.js MD5 hash
@@ -18,6 +21,7 @@
1821
- all packages of the framework moved to ESM
1922
- require Node.js v20
2023
- `@supercharge/contracts`
24+
- removed export `RequestHeaderBag` contract. The `Request` interface uses the `InputBag<IncomingHttpHeaders>` instead
2125
- removed export `RequestStateData`, use `HttpStateData` instead
2226
- `StateBag`: the `has(key)` method now determines whether the value for a given `key` is not `undefined`. If you want to check whether a given `key` is present in the state bag, independently from the value, use the newly added `exists(key)` method
2327
- `StateBag`:
@@ -27,6 +31,8 @@
2731
- `@supercharge/hashing`
2832
- removed `bcrypt` package from being installed automatically, users must install it explicitely when the hashing driver should use bcrypt
2933
- hashing options require a factory function to return the hash driver constructor
34+
- `@supercharge/http`
35+
- the `RequestHeaderBag` extends the `InputBag` which changes the behavior of the `has(key)` method: it returns `false` if the stored value is `undefined` and returns `true` otherwise
3036

3137

3238

packages/contracts/src/http/request-header-bag.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
import { IncomingHttpHeaders } from 'http2'
3+
4+
// copy every declared property from http.IncomingHttpHeaders
5+
// but remove index signatures
6+
7+
/**
8+
* This type copies over all properties from the `IncomingHttpHeaders` type
9+
* except the index signature. The index signature is nice to use custom
10+
* HTTP headers, but it throws away IntelliSense which we want to keep.
11+
*/
12+
export type HttpDefaultRequestHeaders = {
13+
[K in keyof IncomingHttpHeaders as string extends K
14+
? never
15+
: number extends K
16+
? never
17+
: K
18+
]: IncomingHttpHeaders[K];
19+
}
20+
21+
export type HttpDefaultRequestHeader = keyof HttpDefaultRequestHeaders
22+
23+
/**
24+
* This `HttpRequestHeaders` interface can be used to extend the default
25+
* HTTP headers with custom header key-value pairs. The HTTP request
26+
* picks up the custom headers and keeps IntelliSense for the dev.
27+
*
28+
* You can extend this interface in your code like this:
29+
*
30+
* @example
31+
*
32+
* ```ts
33+
* declare module '@supercharge/contracts' {
34+
* export interface HttpRequestHeaders {
35+
* 'your-header-name': string | undefined
36+
* }
37+
* }
38+
* ```
39+
*/
40+
41+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
42+
export interface HttpRequestHeaders extends HttpDefaultRequestHeaders {
43+
//
44+
}
45+
46+
export type HttpRequestHeader = keyof HttpRequestHeaders

packages/contracts/src/http/request.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { InputBag } from './input-bag.js'
44
import { HttpMethods } from './methods.js'
55
import { HttpContext } from './context.js'
66
import { CookieBag } from './cookie-bag.js'
7+
import { IncomingMessage } from 'node:http'
8+
import { IncomingHttpHeaders } from 'node:http2'
79
import { CookieOptions } from './cookie-options.js'
810
import { MacroableCtor } from '@supercharge/macroable'
9-
import { RequestHeaderBag } from './request-header-bag.js'
1011
import { QueryParameterBag } from './query-parameter-bag.js'
11-
import { IncomingHttpHeaders, IncomingMessage } from 'node:http'
1212
import { InteractsWithState } from './concerns/interacts-with-state.js'
1313
import { RequestCookieBuilderCallback } from './cookie-options-builder.js'
1414
import { InteractsWithContentTypes } from './concerns/interacts-with-content-types.js'
@@ -170,7 +170,7 @@ export interface HttpRequest extends InteractsWithState, InteractsWithContentTyp
170170
/**
171171
* Returns the request header bag.
172172
*/
173-
headers(): RequestHeaderBag
173+
headers<RequestHeaders = IncomingHttpHeaders>(): InputBag<RequestHeaders>
174174

175175
/**
176176
* Returns the request header identified by the given `key`. The default

packages/contracts/src/http/response.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

2+
import { InputBag } from './input-bag.js'
23
import { HttpContext } from './context.js'
34
import { CookieBag } from './cookie-bag.js'
5+
import { OutgoingHttpHeaders } from 'http2'
46
import { HttpRedirect } from './redirect.js'
57
import { CookieOptions } from './cookie-options.js'
68
import { MacroableCtor } from '@supercharge/macroable'
@@ -31,16 +33,15 @@ export interface HttpResponse<T = any> extends InteractsWithState {
3133
header (key: string, value: string | string[] | number): this
3234

3335
/**
34-
* Returns the response headers.
36+
* Returns the response headers bag.
3537
*
3638
* @example
3739
* ```
3840
* const responseHeaders = response.header('Content-Type', 'application/json').headers()
3941
* // { 'Content-Type': 'application/json' }
4042
* ```
4143
*/
42-
// headers(): HeaderBag<string | string[] | number>
43-
headers (): any
44+
headers<ResponseHeaders = OutgoingHttpHeaders>(): InputBag<ResponseHeaders>
4445

4546
/**
4647
* Assign the object’s key-value pairs as response headers.

packages/contracts/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export { Middleware, MiddlewareCtor, InlineMiddlewareHandler } from './http/midd
4545
export { PendingRoute } from './http/pending-route.js'
4646
export { HttpRedirect } from './http/redirect.js'
4747
export { HttpRequest, HttpRequestCtor, Protocol } from './http/request.js'
48-
export { RequestHeaderBag } from './http/request-header-bag.js'
48+
export { HttpDefaultRequestHeaders, HttpDefaultRequestHeader, HttpRequestHeaders, HttpRequestHeader } from './http/request-headers.js'
4949
export { HttpResponse, HttpResponseCtor } from './http/response.js'
5050
export { HttpRouteCollection } from './http/route-collection.js'
5151
export { HttpRouteGroup } from './http/route-group.js'

packages/http/src/server/input-bag.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class InputBag<Properties> implements InputBagContract<Properties> {
9494
/**
9595
* Determine whether the given `input` is an object.
9696
*/
97-
private isObject (input: any): input is Record<string, any> {
97+
protected isObject (input: any): input is Record<string, any> {
9898
return !!input && input.constructor.name === 'Object'
9999
}
100100

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,55 @@
11

2-
import { tap } from '@supercharge/goodies'
3-
import { RouterContext } from '@koa/router'
4-
import { IncomingHttpHeaders } from 'node:http'
5-
import { Dict, RequestHeaderBag as RequestHeaderBagContract } from '@supercharge/contracts'
6-
7-
export class RequestHeaderBag implements RequestHeaderBagContract {
8-
/**
9-
* Stores the request headers as an object.
10-
*/
11-
private readonly ctx: RouterContext
12-
13-
/**
14-
* Create a new instance.
15-
*/
16-
constructor (ctx: RouterContext) {
17-
this.ctx = ctx
18-
}
2+
import { InputBag } from './input-bag.js'
193

4+
export class RequestHeaderBag<RequestHeaders> extends InputBag<RequestHeaders> {
205
/**
216
* Returns the lowercased string value for the given `name`.
227
*/
23-
private resolveName (name: keyof IncomingHttpHeaders): string {
24-
return String(name).toLowerCase()
25-
}
26-
27-
/**
28-
* Returns an object with all `keys` existing in the input bag.
29-
*/
30-
all<Key extends keyof IncomingHttpHeaders = string> (...keys: Key[] | Key[][]): { [Key in keyof IncomingHttpHeaders]: IncomingHttpHeaders[Key] } {
31-
if (keys.length === 0) {
32-
return this.ctx.headers
33-
}
34-
35-
return ([] as Key[])
36-
.concat(...keys)
37-
.map(name => this.resolveName(name))
38-
.reduce((carry: Dict<IncomingHttpHeaders[Key]>, key) => {
39-
carry[key] = this.get(key)
40-
41-
return carry
42-
}, {})
8+
private lowercase<Key extends keyof RequestHeaders = any> (name: string | Key): Key {
9+
return String(name).toLowerCase() as Key
4310
}
4411

4512
/**
4613
* Returns the input value for the given `name`. Returns `undefined`
4714
* if the given `name` does not exist in the input bag.
4815
*/
49-
get<Header extends keyof IncomingHttpHeaders> (name: Header): IncomingHttpHeaders[Header]
50-
get<T, Header extends keyof IncomingHttpHeaders> (name: Header, defaultValue: T): IncomingHttpHeaders[Header] | T
51-
get<T, Header extends keyof IncomingHttpHeaders> (name: Header, defaultValue?: T): IncomingHttpHeaders[Header] | T {
52-
const key = this.resolveName(name)
53-
16+
override get<Value = any, Key extends keyof RequestHeaders = any> (key: Key, defaultValue?: Value): RequestHeaders[Key] | Value | undefined {
5417
switch (key) {
5518
case 'referrer':
5619
case 'referer':
57-
return this.ctx.request.headers.referrer ?? this.ctx.request.headers.referer ?? defaultValue
20+
return super.get('referrer' as Key) ?? super.get('referer' as Key) ?? defaultValue
21+
5822
default:
59-
return this.ctx.request.headers[key] ?? defaultValue
23+
return super.get(this.lowercase<Key>(key), defaultValue)
6024
}
6125
}
6226

6327
/**
6428
* Set an input for the given `name` and assign the `value`. This
6529
* overrides a possibly existing input with the same `name`.
6630
*/
67-
set (name: string, value: any): this {
68-
const key = this.resolveName(name)
69-
70-
return tap(this, () => {
71-
this.ctx.request.headers[key] = value
72-
})
73-
}
74-
75-
/**
76-
* Removes the input with the given `name`.
77-
*/
78-
remove (name: string): this {
79-
const key = this.resolveName(name)
31+
override set<Key extends keyof RequestHeaders> (key: Key | Partial<RequestHeaders>, value?: any): this {
32+
if (this.isObject(key)) {
33+
const values = Object.entries(key).reduce<Partial<RequestHeaders>>((carry, [key, value]) => {
34+
const id = this.lowercase<Key>(key)
35+
// @ts-expect-error
36+
carry[id] = value
8037

81-
return tap(this, () => {
82-
const { [key]: _, ...rest } = this.ctx.request.headers
38+
return carry
39+
}, {})
8340

84-
this.ctx.request.headers = rest
85-
})
86-
}
41+
return super.set(values)
42+
}
8743

88-
/**
89-
* Determine whether the HTTP header for the given `name` exists.
90-
*/
91-
has (name: keyof IncomingHttpHeaders): name is keyof IncomingHttpHeaders {
92-
return !!this.get(name)
44+
return super.set(this.lowercase(key), value)
9345
}
9446

9547
/**
96-
* Returns an object containing all parameters.
48+
* Removes the input with the given `name`.
9749
*/
98-
toJSON (): Partial<IncomingHttpHeaders> {
99-
return this.all()
50+
override remove<Key extends keyof RequestHeaders> (key: Key): this {
51+
return super.remove(
52+
this.lowercase(key)
53+
)
10054
}
10155
}

0 commit comments

Comments
 (0)