Skip to content

Commit

Permalink
Merge pull request #148 from supercharge/header-bag-refactoring
Browse files Browse the repository at this point in the history
HeaderBag extends InputBag refactoring
  • Loading branch information
marcuspoehls authored Nov 3, 2023
2 parents 247d102 + fd2f452 commit 6e1b5d6
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 234 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## [4.0.0](https://github.com/supercharge/framework/compare/v3.20.4...v4.0.0) - 2023-xx-xx

### Added
- `@supercharge/contracts`
- 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
- 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
- `@supercharge/hashing`
- add `createHash` method: create a Node.js `Hash` instance for a given input
- add `md5` method: create a Node.js MD5 hash
Expand All @@ -18,6 +21,7 @@
- all packages of the framework moved to ESM
- require Node.js v20
- `@supercharge/contracts`
- removed export `RequestHeaderBag` contract. The `Request` interface uses the `InputBag<IncomingHttpHeaders>` instead
- removed export `RequestStateData`, use `HttpStateData` instead
- `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
- `StateBag`:
Expand All @@ -27,6 +31,8 @@
- `@supercharge/hashing`
- removed `bcrypt` package from being installed automatically, users must install it explicitely when the hashing driver should use bcrypt
- hashing options require a factory function to return the hash driver constructor
- `@supercharge/http`
- 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



Expand Down
37 changes: 0 additions & 37 deletions packages/contracts/src/http/request-header-bag.ts

This file was deleted.

46 changes: 46 additions & 0 deletions packages/contracts/src/http/request-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@

import { IncomingHttpHeaders } from 'http2'

// copy every declared property from http.IncomingHttpHeaders
// but remove index signatures

/**
* This type copies over all properties from the `IncomingHttpHeaders` type
* except the index signature. The index signature is nice to use custom
* HTTP headers, but it throws away IntelliSense which we want to keep.
*/
export type HttpDefaultRequestHeaders = {
[K in keyof IncomingHttpHeaders as string extends K
? never
: number extends K
? never
: K
]: IncomingHttpHeaders[K];
}

export type HttpDefaultRequestHeader = keyof HttpDefaultRequestHeaders

/**
* This `HttpRequestHeaders` interface can be used to extend the default
* HTTP headers with custom header key-value pairs. The HTTP request
* picks up the custom headers and keeps IntelliSense for the dev.
*
* You can extend this interface in your code like this:
*
* @example
*
* ```ts
* declare module '@supercharge/contracts' {
* export interface HttpRequestHeaders {
* 'your-header-name': string | undefined
* }
* }
* ```
*/

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface HttpRequestHeaders extends HttpDefaultRequestHeaders {
//
}

export type HttpRequestHeader = keyof HttpRequestHeaders
6 changes: 3 additions & 3 deletions packages/contracts/src/http/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { InputBag } from './input-bag.js'
import { HttpMethods } from './methods.js'
import { HttpContext } from './context.js'
import { CookieBag } from './cookie-bag.js'
import { IncomingMessage } from 'node:http'
import { IncomingHttpHeaders } from 'node:http2'
import { CookieOptions } from './cookie-options.js'
import { MacroableCtor } from '@supercharge/macroable'
import { RequestHeaderBag } from './request-header-bag.js'
import { QueryParameterBag } from './query-parameter-bag.js'
import { IncomingHttpHeaders, IncomingMessage } from 'node:http'
import { InteractsWithState } from './concerns/interacts-with-state.js'
import { RequestCookieBuilderCallback } from './cookie-options-builder.js'
import { InteractsWithContentTypes } from './concerns/interacts-with-content-types.js'
Expand Down Expand Up @@ -170,7 +170,7 @@ export interface HttpRequest extends InteractsWithState, InteractsWithContentTyp
/**
* Returns the request header bag.
*/
headers(): RequestHeaderBag
headers<RequestHeaders = IncomingHttpHeaders>(): InputBag<RequestHeaders>

/**
* Returns the request header identified by the given `key`. The default
Expand Down
7 changes: 4 additions & 3 deletions packages/contracts/src/http/response.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

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

/**
* Returns the response headers.
* Returns the response headers bag.
*
* @example
* ```
* const responseHeaders = response.header('Content-Type', 'application/json').headers()
* // { 'Content-Type': 'application/json' }
* ```
*/
// headers(): HeaderBag<string | string[] | number>
headers (): any
headers<ResponseHeaders = OutgoingHttpHeaders>(): InputBag<ResponseHeaders>

/**
* Assign the object’s key-value pairs as response headers.
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export { Middleware, MiddlewareCtor, InlineMiddlewareHandler } from './http/midd
export { PendingRoute } from './http/pending-route.js'
export { HttpRedirect } from './http/redirect.js'
export { HttpRequest, HttpRequestCtor, Protocol } from './http/request.js'
export { RequestHeaderBag } from './http/request-header-bag.js'
export { HttpDefaultRequestHeaders, HttpDefaultRequestHeader, HttpRequestHeaders, HttpRequestHeader } from './http/request-headers.js'
export { HttpResponse, HttpResponseCtor } from './http/response.js'
export { HttpRouteCollection } from './http/route-collection.js'
export { HttpRouteGroup } from './http/route-group.js'
Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/server/input-bag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class InputBag<Properties> implements InputBagContract<Properties> {
/**
* Determine whether the given `input` is an object.
*/
private isObject (input: any): input is Record<string, any> {
protected isObject (input: any): input is Record<string, any> {
return !!input && input.constructor.name === 'Object'
}

Expand Down
94 changes: 24 additions & 70 deletions packages/http/src/server/request-header-bag.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,55 @@

import { tap } from '@supercharge/goodies'
import { RouterContext } from '@koa/router'
import { IncomingHttpHeaders } from 'node:http'
import { Dict, RequestHeaderBag as RequestHeaderBagContract } from '@supercharge/contracts'

export class RequestHeaderBag implements RequestHeaderBagContract {
/**
* Stores the request headers as an object.
*/
private readonly ctx: RouterContext

/**
* Create a new instance.
*/
constructor (ctx: RouterContext) {
this.ctx = ctx
}
import { InputBag } from './input-bag.js'

export class RequestHeaderBag<RequestHeaders> extends InputBag<RequestHeaders> {
/**
* Returns the lowercased string value for the given `name`.
*/
private resolveName (name: keyof IncomingHttpHeaders): string {
return String(name).toLowerCase()
}

/**
* Returns an object with all `keys` existing in the input bag.
*/
all<Key extends keyof IncomingHttpHeaders = string> (...keys: Key[] | Key[][]): { [Key in keyof IncomingHttpHeaders]: IncomingHttpHeaders[Key] } {
if (keys.length === 0) {
return this.ctx.headers
}

return ([] as Key[])
.concat(...keys)
.map(name => this.resolveName(name))
.reduce((carry: Dict<IncomingHttpHeaders[Key]>, key) => {
carry[key] = this.get(key)

return carry
}, {})
private lowercase<Key extends keyof RequestHeaders = any> (name: string | Key): Key {
return String(name).toLowerCase() as Key
}

/**
* Returns the input value for the given `name`. Returns `undefined`
* if the given `name` does not exist in the input bag.
*/
get<Header extends keyof IncomingHttpHeaders> (name: Header): IncomingHttpHeaders[Header]
get<T, Header extends keyof IncomingHttpHeaders> (name: Header, defaultValue: T): IncomingHttpHeaders[Header] | T
get<T, Header extends keyof IncomingHttpHeaders> (name: Header, defaultValue?: T): IncomingHttpHeaders[Header] | T {
const key = this.resolveName(name)

override get<Value = any, Key extends keyof RequestHeaders = any> (key: Key, defaultValue?: Value): RequestHeaders[Key] | Value | undefined {
switch (key) {
case 'referrer':
case 'referer':
return this.ctx.request.headers.referrer ?? this.ctx.request.headers.referer ?? defaultValue
return super.get('referrer' as Key) ?? super.get('referer' as Key) ?? defaultValue

default:
return this.ctx.request.headers[key] ?? defaultValue
return super.get(this.lowercase<Key>(key), defaultValue)
}
}

/**
* Set an input for the given `name` and assign the `value`. This
* overrides a possibly existing input with the same `name`.
*/
set (name: string, value: any): this {
const key = this.resolveName(name)

return tap(this, () => {
this.ctx.request.headers[key] = value
})
}

/**
* Removes the input with the given `name`.
*/
remove (name: string): this {
const key = this.resolveName(name)
override set<Key extends keyof RequestHeaders> (key: Key | Partial<RequestHeaders>, value?: any): this {
if (this.isObject(key)) {
const values = Object.entries(key).reduce<Partial<RequestHeaders>>((carry, [key, value]) => {
const id = this.lowercase<Key>(key)
// @ts-expect-error
carry[id] = value

return tap(this, () => {
const { [key]: _, ...rest } = this.ctx.request.headers
return carry
}, {})

this.ctx.request.headers = rest
})
}
return super.set(values)
}

/**
* Determine whether the HTTP header for the given `name` exists.
*/
has (name: keyof IncomingHttpHeaders): name is keyof IncomingHttpHeaders {
return !!this.get(name)
return super.set(this.lowercase(key), value)
}

/**
* Returns an object containing all parameters.
* Removes the input with the given `name`.
*/
toJSON (): Partial<IncomingHttpHeaders> {
return this.all()
override remove<Key extends keyof RequestHeaders> (key: Key): this {
return super.remove(
this.lowercase(key)
)
}
}
Loading

0 comments on commit 6e1b5d6

Please sign in to comment.