2ba352da39
* Initial move * Make emitting work * Update paths * Remove leftover files * Add correct externals configuration * Import correct path * Update path to work with ts-server test * Update lib directory * Compile next-server/lib
273 lines
6.6 KiB
TypeScript
273 lines
6.6 KiB
TypeScript
import { IncomingMessage } from 'http'
|
|
import { NextApiResponse, NextApiRequest } from '../lib/utils'
|
|
import { Stream } from 'stream'
|
|
import getRawBody from 'raw-body'
|
|
import { parse } from 'content-type'
|
|
import { Params } from './router'
|
|
import { PageConfig } from '../../types'
|
|
import { interopDefault } from './load-components'
|
|
|
|
export type NextApiRequestCookies = { [key: string]: string }
|
|
export type NextApiRequestQuery = { [key: string]: string | string[] }
|
|
|
|
export async function apiResolver(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
params: any,
|
|
resolverModule: any
|
|
) {
|
|
try {
|
|
let config: PageConfig = {}
|
|
let bodyParser = true
|
|
if (!resolverModule) {
|
|
res.statusCode = 404
|
|
res.end('Not Found')
|
|
return
|
|
}
|
|
|
|
if (resolverModule.config) {
|
|
config = resolverModule.config
|
|
if (config.api && config.api.bodyParser === false) {
|
|
bodyParser = false
|
|
}
|
|
}
|
|
// Parsing of cookies
|
|
setLazyProp({ req }, 'cookies', getCookieParser(req))
|
|
// Parsing query string
|
|
setLazyProp({ req, params }, 'query', getQueryParser(req))
|
|
// // Parsing of body
|
|
if (bodyParser) {
|
|
req.body = await parseBody(
|
|
req,
|
|
config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit
|
|
? config.api.bodyParser.sizeLimit
|
|
: '1mb'
|
|
)
|
|
}
|
|
|
|
res.status = statusCode => sendStatusCode(res, statusCode)
|
|
res.send = data => sendData(res, data)
|
|
res.json = data => sendJson(res, data)
|
|
|
|
const resolver = interopDefault(resolverModule)
|
|
resolver(req, res)
|
|
} catch (e) {
|
|
if (e instanceof ApiError) {
|
|
sendError(res, e.statusCode, e.message)
|
|
} else {
|
|
console.error(e)
|
|
sendError(res, 500, 'Internal Server Error')
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse incoming message like `json` or `urlencoded`
|
|
* @param req request object
|
|
*/
|
|
export async function parseBody(req: NextApiRequest, limit: string | number) {
|
|
const contentType = parse(req.headers['content-type'] || 'text/plain')
|
|
const { type, parameters } = contentType
|
|
const encoding = parameters.charset || 'utf-8'
|
|
|
|
let buffer
|
|
|
|
try {
|
|
buffer = await getRawBody(req, { encoding, limit })
|
|
} catch (e) {
|
|
if (e.type === 'entity.too.large') {
|
|
throw new ApiError(413, `Body exceeded ${limit} limit`)
|
|
} else {
|
|
throw new ApiError(400, 'Invalid body')
|
|
}
|
|
}
|
|
|
|
const body = buffer.toString()
|
|
|
|
if (type === 'application/json' || type === 'application/ld+json') {
|
|
return parseJson(body)
|
|
} else if (type === 'application/x-www-form-urlencoded') {
|
|
const qs = require('querystring')
|
|
return qs.decode(body)
|
|
} else {
|
|
return body
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse `JSON` and handles invalid `JSON` strings
|
|
* @param str `JSON` string
|
|
*/
|
|
function parseJson(str: string) {
|
|
try {
|
|
return JSON.parse(str)
|
|
} catch (e) {
|
|
throw new ApiError(400, 'Invalid JSON')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parsing query arguments from request `url` string
|
|
* @param url of request
|
|
* @returns Object with key name of query argument and its value
|
|
*/
|
|
export function getQueryParser({ url }: IncomingMessage) {
|
|
return function parseQuery(): NextApiRequestQuery {
|
|
const { URL } = require('url')
|
|
// we provide a placeholder base url because we only want searchParams
|
|
const params = new URL(url, 'https://n').searchParams
|
|
|
|
const query: { [key: string]: string | string[] } = {}
|
|
for (const [key, value] of params) {
|
|
query[key] = value
|
|
}
|
|
|
|
return query
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse cookeies from `req` header
|
|
* @param req request object
|
|
*/
|
|
export function getCookieParser(req: IncomingMessage) {
|
|
return function parseCookie(): NextApiRequestCookies {
|
|
const header: undefined | string | string[] = req.headers.cookie
|
|
|
|
if (!header) {
|
|
return {}
|
|
}
|
|
|
|
const { parse } = require('cookie')
|
|
return parse(Array.isArray(header) ? header.join(';') : header)
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param res response object
|
|
* @param statusCode `HTTP` status code of response
|
|
*/
|
|
export function sendStatusCode(res: NextApiResponse, statusCode: number) {
|
|
res.statusCode = statusCode
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* Send `any` body to response
|
|
* @param res response object
|
|
* @param body of response
|
|
*/
|
|
export function sendData(res: NextApiResponse, body: any) {
|
|
if (body === null) {
|
|
res.end()
|
|
return
|
|
}
|
|
|
|
const contentType = res.getHeader('Content-Type')
|
|
|
|
if (Buffer.isBuffer(body)) {
|
|
if (!contentType) {
|
|
res.setHeader('Content-Type', 'application/octet-stream')
|
|
}
|
|
res.setHeader('Content-Length', body.length)
|
|
res.end(body)
|
|
return
|
|
}
|
|
|
|
if (body instanceof Stream) {
|
|
if (!contentType) {
|
|
res.setHeader('Content-Type', 'application/octet-stream')
|
|
}
|
|
body.pipe(res)
|
|
return
|
|
}
|
|
|
|
let str = body
|
|
|
|
// Stringify JSON body
|
|
if (typeof body === 'object' || typeof body === 'number') {
|
|
str = JSON.stringify(body)
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
}
|
|
|
|
res.setHeader('Content-Length', Buffer.byteLength(str))
|
|
res.end(str)
|
|
}
|
|
|
|
/**
|
|
* Send `JSON` object
|
|
* @param res response object
|
|
* @param jsonBody of data
|
|
*/
|
|
export function sendJson(res: NextApiResponse, jsonBody: any): void {
|
|
// Set header to application/json
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
|
|
// Use send to handle request
|
|
res.send(jsonBody)
|
|
}
|
|
|
|
/**
|
|
* Custom error class
|
|
*/
|
|
export class ApiError extends Error {
|
|
readonly statusCode: number
|
|
|
|
constructor(statusCode: number, message: string) {
|
|
super(message)
|
|
this.statusCode = statusCode
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends error in `response`
|
|
* @param res response object
|
|
* @param statusCode of response
|
|
* @param message of response
|
|
*/
|
|
export function sendError(
|
|
res: NextApiResponse,
|
|
statusCode: number,
|
|
message: string
|
|
) {
|
|
res.statusCode = statusCode
|
|
res.statusMessage = message
|
|
res.end(message)
|
|
}
|
|
|
|
interface LazyProps {
|
|
req: NextApiRequest
|
|
params?: Params | boolean
|
|
}
|
|
|
|
/**
|
|
* Execute getter function only if its needed
|
|
* @param LazyProps `req` and `params` for lazyProp
|
|
* @param prop name of property
|
|
* @param getter function to get data
|
|
*/
|
|
export function setLazyProp<T>(
|
|
{ req, params }: LazyProps,
|
|
prop: string,
|
|
getter: () => T
|
|
) {
|
|
const opts = { configurable: true, enumerable: true }
|
|
const optsReset = { ...opts, writable: true }
|
|
|
|
Object.defineProperty(req, prop, {
|
|
...opts,
|
|
get: () => {
|
|
let value = getter()
|
|
if (params && typeof params !== 'boolean') {
|
|
value = { ...value, ...params }
|
|
}
|
|
// we set the property on the object to avoid recalculating it
|
|
Object.defineProperty(req, prop, { ...optsReset, value })
|
|
return value
|
|
},
|
|
set: value => {
|
|
Object.defineProperty(req, prop, { ...optsReset, value })
|
|
},
|
|
})
|
|
}
|