Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add request and response utils #15

Merged
merged 11 commits into from
Dec 12, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"release": "yarn test && yarn build && standard-version && npm publish && git push --follow-tags",
"test": "yarn lint && jest"
},
"dependencies": {
"destr": "^1.0.1"
},
"devDependencies": {
"@nuxtjs/eslint-config-typescript": "latest",
"@types/express": "^4.17.9",
Expand Down
6 changes: 5 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ export function createHandle (stack: Stack): PHandle {
}
}
if (!res.writableEnded) {
throw createError(404, 'Not Found')
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
runtime: true
})
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,17 @@ export interface App {
export interface AppOptions {
debug?: boolean
}

export interface HttpError {
statusCode: number
statusMessage: string
body?: any
}

export interface RuntimeError extends HttpError {
runtime: true
}

export interface InternalError extends HttpError {
internal: true
}
89 changes: 81 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ServerResponse } from 'http'
import { PHandle } from './types'
import type { ServerResponse, IncomingMessage } from 'http'
import { URL, URLSearchParams } from 'url'
import destr from 'destr'
import { InternalError, PHandle, RuntimeError } from './types'

export const MIMES = {
html: 'text/html',
Expand All @@ -26,22 +28,43 @@ export function sendError (res: ServerResponse, error: Error | string, code?: nu
error.statusCode || error.status ||
500

if (debug && res.statusCode !== 404) {
// @ts-ignore
if (debug && !error.runtime && res.statusCode !== 404) {
console.error(error) // eslint-disable-line no-console
}

// @ts-ignore
res.statusMessage = res.statusMessage || error.statusMessage || error.statusText || 'Internal Error'

res.end(`"${res.statusMessage} (${res.statusCode})"`)
// @ts-ignore
let body: any = {
statusCode: res.statusCode,
statusMessage: res.statusMessage
}
// @ts-ignore
if (debug || error.runtime) {
// @ts-ignore
body = error.body || body
}
if (typeof body === 'object') {
res.setHeader('Content-Type', MIMES.json)
body = JSON.stringify(body)
}
res.end(body)
}

export function createError (statusCode: number, statusMessage: string) {
const err = new Error(statusMessage)
export function createError (runtimeError: InternalError | RuntimeError) {
const err = new Error(runtimeError.statusMessage)
// @ts-ignore
err.statusCode = statusCode
err.statusCode = runtimeError.statusCode
// @ts-ignore
err.statusMessage = statusMessage
err.statusMessage = runtimeError.statusMessage
// @ts-ignore
err.body = runtimeError.body
// @ts-ignore
err.runtime = runtimeError.runtime || false
// @ts-ignore
err.internal = runtimeError.internal || false
return err
}

Expand All @@ -68,3 +91,53 @@ export function useBase (base: string, handle: PHandle): PHandle {
return handle(req, res)
}
}

export function getParams (req: IncomingMessage): URLSearchParams {
const url = new URL(req.url || '', 'http://localhost')
return url.searchParams
pi0 marked this conversation as resolved.
Show resolved Hide resolved
}

export function getQuery (req: IncomingMessage) {
const params = getParams(req)
const query: { [key: string]: string | string[] } = {}
for (const [name, value] of params) {
if (typeof query[name] === 'undefined') {
query[name] = value
} else if (typeof query[name] === 'string') {
query[name] = [query[name] as string]
}
if (Array.isArray(query[name])) {
(query[name] as string[]).push(value)
}
}
return query
}

export function getBody (req: IncomingMessage): Promise<string> {
// @ts-ignore
if (req.body) { return req.body }
return new Promise<string>((resolve, reject) => {
const bodyData: any[] = []
req.on('error', (err) => {
reject(err)
}).on('data', (chunk) => {
bodyData.push(chunk)
}).on('end', () => {
const body = Buffer.concat(bodyData).toString()
// @ts-ignore
req.body = body
resolve(body)
})
})
}

export async function getJSON<T> (req: IncomingMessage): Promise<T> {
// @ts-ignore
if (req.bodyJSON) { return req.bodyJSON }

const body = await getBody(req)
const json = destr(body)
// @ts-ignore
req.bodyJSON = json
return json
}
86 changes: 85 additions & 1 deletion test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import supertest, { SuperTest, Test } from 'supertest'
import { createApp, App, sendError, sendRedirect, stripTrailingSlash, useBase } from '../src'
import { createApp, App, sendError, createError, sendRedirect, stripTrailingSlash, useBase, getBody, MIMES, getJSON } from '../src'

;(global.console.error as any) = jest.fn()

Expand Down Expand Up @@ -75,4 +75,88 @@ describe('', () => {
expect(result.text).toBe('/api/test')
})
})

describe('getBody', () => {
it('can parse json payload', async () => {
app.use('/', async (request) => {
const body = await getJSON(request)
expect(body).toMatchObject({
bool: true,
name: 'string',
number: 1
})
return '200'
})
const result = await request.post('/api/test').send({
bool: true,
name: 'string',
number: 1
})

expect(result.text).toBe('200')
})

it('can handle raw string', async () => {
app.use('/', async (request) => {
const body = await getBody(request)
expect(body).toEqual('{"bool":true,"name":"string","number":1}')
return '200'
})
const result = await request.post('/api/test').send(JSON.stringify({
bool: true,
name: 'string',
number: 1
}))

expect(result.text).toBe('200')
})
})

describe('createError', () => {
it('can sent internal error', async () => {
app.use('/', () => {
throw createError({
statusCode: 500,
statusMessage: 'Internal error',
body: 'oops',
internal: true
})
})
const result = await request.get('/api/test')

expect(result.status).toBe(500)
// eslint-disable-next-line
expect(console.error).toBeCalled()

expect(result.text).toBe(JSON.stringify({
statusCode: 500,
statusMessage: 'Internal error'
}))
})

it('can sent runtime error', async () => {
jest.clearAllMocks()

app.use('/', () => {
throw createError({
statusCode: 400,
statusMessage: 'Bad Request',
body: {
message: 'Invalid Input'
},
runtime: true
})
})
const result = await request.get('/api/test')

expect(result.status).toBe(400)
expect(result.type).toBe(MIMES.json)
// eslint-disable-next-line
expect(console.error).not.toBeCalled()

expect(result.text).toBe(JSON.stringify({
message: 'Invalid Input'
}))
})
})
})
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=

destr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/destr/-/destr-1.0.1.tgz#d13db7f9d9c9ca4fcf24e86343d601217136ddc3"
integrity sha512-LnEdINrd1ydSqRiAGjMBVrG/G8hNruwE+fEKlkJA14MGPEoI9T7zJDwGpkMTyXT2ASE0ycnN2SYn4k6Q7j7lHg==

destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
Expand Down