@@ -1,17 +0,0 @@ | |||||
import {IncomingMessage} from 'http'; | |||||
export const getBody = (req: IncomingMessage) => new Promise<Buffer>((resolve, reject) => { | |||||
let body = Buffer.from(''); | |||||
req.on('data', (chunk) => { | |||||
body = Buffer.concat([body, chunk]); | |||||
}); | |||||
req.on('error', (err) => { | |||||
reject(err); | |||||
}); | |||||
req.on('end', () => { | |||||
resolve(body); | |||||
}); | |||||
}); |
@@ -3,6 +3,15 @@ | |||||
"extends": [ | "extends": [ | ||||
"lxsmnsyc/typescript/react" | "lxsmnsyc/typescript/react" | ||||
], | ], | ||||
"rules": { | |||||
"no-tabs": "off", | |||||
"indent": ["error", "tab"], | |||||
"react/jsx-indent-props": ["error", "tab"], | |||||
"react/jsx-indent": ["error", "tab"], | |||||
"react/jsx-props-no-spreading": "off", | |||||
"@typescript-eslint/no-misused-promises": "off", | |||||
"@typescript-eslint/no-namespace": "off" | |||||
}, | |||||
"parserOptions": { | "parserOptions": { | ||||
"project": "./tsconfig.eslint.json" | "project": "./tsconfig.eslint.json" | ||||
} | } | ||||
@@ -67,7 +67,8 @@ | |||||
"@theoryofnekomata/formxtra": "^1.0.3", | "@theoryofnekomata/formxtra": "^1.0.3", | ||||
"busboy": "^1.6.0", | "busboy": "^1.6.0", | ||||
"fetch-ponyfill": "^7.1.0", | "fetch-ponyfill": "^7.1.0", | ||||
"nookies": "^2.5.2" | |||||
"nookies": "^2.5.2", | |||||
"seroval": "^0.9.0" | |||||
}, | }, | ||||
"types": "./dist/types/index.d.ts", | "types": "./dist/types/index.d.ts", | ||||
"main": "./dist/cjs/production/index.js", | "main": "./dist/cjs/production/index.js", | ||||
@@ -1,8 +1,14 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { getFormValues } from '@theoryofnekomata/formxtra'; | import { getFormValues } from '@theoryofnekomata/formxtra'; | ||||
import fetchPonyfill from 'fetch-ponyfill'; | import fetchPonyfill from 'fetch-ponyfill'; | ||||
import {NextPage as DefaultNextPage} from 'next'; | |||||
import {NextApiResponse, NextApiRequest} from './common'; | |||||
import { NextPage as DefaultNextPage } from 'next'; | |||||
import { | |||||
NextApiResponse, | |||||
NextApiRequest, | |||||
ENCTYPE_APPLICATION_JSON, | |||||
ENCTYPE_MULTIPART_FORM_DATA, | |||||
ENCTYPE_X_WWW_FORM_URLENCODED, | |||||
} from './common'; | |||||
const FormDerivedElementComponent = 'form' as const; | const FormDerivedElementComponent = 'form' as const; | ||||
@@ -15,12 +21,12 @@ type EncTypeSerializer = (data: unknown) => string; | |||||
type EncTypeSerializerMap = Record<string, EncTypeSerializer>; | type EncTypeSerializerMap = Record<string, EncTypeSerializer>; | ||||
const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | ||||
'application/json': (data: unknown) => JSON.stringify(data), | |||||
[ENCTYPE_APPLICATION_JSON]: (data: unknown) => JSON.stringify(data), | |||||
}; | }; | ||||
type GetFormValuesOptions = Parameters<typeof getFormValues>[1]; | type GetFormValuesOptions = Parameters<typeof getFormValues>[1]; | ||||
interface FormProps extends Omit<BaseProps, 'action'> { | |||||
export interface FormProps extends Omit<BaseProps, 'action'> { | |||||
action?: string; | action?: string; | ||||
clientAction?: string; | clientAction?: string; | ||||
clientHeaders?: HeadersInit; | clientHeaders?: HeadersInit; | ||||
@@ -34,23 +40,25 @@ interface FormProps extends Omit<BaseProps, 'action'> { | |||||
export const useResponse = (res: NextApiResponse) => { | export const useResponse = (res: NextApiResponse) => { | ||||
const [response, setResponse] = React.useState<Response | undefined>( | const [response, setResponse] = React.useState<Response | undefined>( | ||||
res.body ? new Response(res.body as any) : undefined | |||||
res.body ? new Response(res.body as unknown as BodyInit) : undefined, | |||||
); | ); | ||||
const onStale = React.useCallback(() => { | |||||
const invalidate = React.useCallback(() => { | |||||
setResponse(undefined); | setResponse(undefined); | ||||
}, []); | }, []); | ||||
const onFresh = React.useCallback((response: Response) => { | |||||
setResponse(response); | |||||
const refresh = React.useCallback((newResponse: Response) => { | |||||
setResponse(newResponse); | |||||
}, []); | }, []); | ||||
return React.useMemo(() => ({ | return React.useMemo(() => ({ | ||||
response, | response, | ||||
refresh: onFresh, | |||||
invalidate: onStale, | |||||
refresh, | |||||
invalidate, | |||||
}), [ | }), [ | ||||
response, | response, | ||||
refresh, | |||||
invalidate, | |||||
]); | ]); | ||||
}; | }; | ||||
@@ -66,18 +74,18 @@ const serializeBody = (params: SerializeBodyParams) => { | |||||
form, | form, | ||||
encType, | encType, | ||||
serializers, | serializers, | ||||
options | |||||
options, | |||||
} = params; | } = params; | ||||
if (encType === 'multipart/form-data') { | |||||
if (encType === ENCTYPE_MULTIPART_FORM_DATA) { | |||||
// type error when provided a submitter element for some reason... | // type error when provided a submitter element for some reason... | ||||
const FormDataUnknown = FormData as unknown as { | const FormDataUnknown = FormData as unknown as { | ||||
new(form?: HTMLElement, submitter?: HTMLElement ): BodyInit; | |||||
new(formElement?: HTMLElement, submitter?: HTMLElement): BodyInit; | |||||
}; | }; | ||||
return new FormDataUnknown(form, options?.submitter); | return new FormDataUnknown(form, options?.submitter); | ||||
} | } | ||||
if (encType === 'application/x-www-form-urlencoded') { | |||||
if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||||
return new URLSearchParams(form); | return new URLSearchParams(form); | ||||
} | } | ||||
@@ -86,7 +94,7 @@ const serializeBody = (params: SerializeBodyParams) => { | |||||
} | } | ||||
throw new Error(`Unsupported encType: ${encType}`); | throw new Error(`Unsupported encType: ${encType}`); | ||||
} | |||||
}; | |||||
export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | ||||
children, | children, | ||||
@@ -96,11 +104,11 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
clientAction = action, | clientAction = action, | ||||
clientMethod = method, | clientMethod = method, | ||||
clientHeaders, | clientHeaders, | ||||
encType = 'multipart/form-data', | |||||
encType = ENCTYPE_MULTIPART_FORM_DATA, | |||||
invalidate, | invalidate, | ||||
refresh, | refresh, | ||||
encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS, | encTypeSerializers = DEFAULT_ENCTYPE_SERIALIZERS, | ||||
responseEncType = 'application/json', | |||||
responseEncType = ENCTYPE_APPLICATION_JSON, | |||||
serializerOptions, | serializerOptions, | ||||
...etcProps | ...etcProps | ||||
}, forwardedRef) => { | }, forwardedRef) => { | ||||
@@ -113,15 +121,20 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
const { fetch } = fetchPonyfill(); | const { fetch } = fetchPonyfill(); | ||||
const headers: HeadersInit = { | const headers: HeadersInit = { | ||||
...(clientHeaders ?? {}), | ...(clientHeaders ?? {}), | ||||
'Accept': responseEncType, | |||||
Accept: responseEncType, | |||||
}; | }; | ||||
if (encType !== 'multipart/form-data') { | |||||
if (encType !== ENCTYPE_MULTIPART_FORM_DATA) { | |||||
// browser automatically generates content-type header for multipart/form-data | // browser automatically generates content-type header for multipart/form-data | ||||
(headers as unknown as Record<string, string>)['Content-Type'] = encType; | (headers as unknown as Record<string, string>)['Content-Type'] = encType; | ||||
} | } | ||||
const response = await fetch(clientAction, { | |||||
const fetchInit: RequestInit = { | |||||
method: clientMethod.toUpperCase(), | method: clientMethod.toUpperCase(), | ||||
body: serializeBody({ | |||||
headers, | |||||
}; | |||||
if (!['GET', 'HEAD'].includes(clientMethod.toUpperCase())) { | |||||
fetchInit.body = serializeBody({ | |||||
form: event.currentTarget, | form: event.currentTarget, | ||||
encType, | encType, | ||||
serializers: encTypeSerializers, | serializers: encTypeSerializers, | ||||
@@ -129,9 +142,9 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
...serializerOptions, | ...serializerOptions, | ||||
submitter: nativeEvent.submitter, | submitter: nativeEvent.submitter, | ||||
}, | }, | ||||
}), | |||||
headers, | |||||
}); | |||||
}); | |||||
} | |||||
const response = await fetch(clientAction, fetchInit); | |||||
refresh?.(response); | refresh?.(response); | ||||
} | } | ||||
@@ -144,7 +157,6 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
const serverMethodRaw = method.toLowerCase(); | const serverMethodRaw = method.toLowerCase(); | ||||
const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; | const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; | ||||
const serverEncType = 'multipart/form-data'; | |||||
return ( | return ( | ||||
<FormDerivedElementComponent | <FormDerivedElementComponent | ||||
@@ -153,14 +165,28 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
onSubmit={handleSubmit} | onSubmit={handleSubmit} | ||||
action={action} | action={action} | ||||
method={serverMethod} | method={serverMethod} | ||||
encType={serverEncType} | |||||
encType={ENCTYPE_MULTIPART_FORM_DATA} | |||||
> | > | ||||
{children} | {children} | ||||
</FormDerivedElementComponent> | </FormDerivedElementComponent> | ||||
); | ); | ||||
}); | }); | ||||
export type NextPage<T = {}, U = T> = DefaultNextPage< | |||||
Form.displayName = 'Form' as const; | |||||
Form.defaultProps = { | |||||
action: undefined, | |||||
clientAction: undefined, | |||||
clientHeaders: undefined, | |||||
clientMethod: undefined, | |||||
encTypeSerializers: DEFAULT_ENCTYPE_SERIALIZERS, | |||||
invalidate: undefined, | |||||
refresh: undefined, | |||||
responseEncType: ENCTYPE_APPLICATION_JSON, | |||||
serializerOptions: undefined, | |||||
}; | |||||
export type NextPage<T = NonNullable<unknown>, U = T> = DefaultNextPage< | |||||
T & { | T & { | ||||
res: NextApiResponse; | res: NextApiResponse; | ||||
req: NextApiRequest; | req: NextApiRequest; | ||||
@@ -1,10 +1,17 @@ | |||||
import {ParsedUrlQuery} from 'querystring'; | |||||
import { | |||||
NextApiRequest as DefaultNextApiRequest, | |||||
} from 'next'; | |||||
export interface NextApiRequest { | |||||
query: ParsedUrlQuery; | |||||
body?: unknown; | |||||
} | |||||
export type NextApiRequest = Pick<DefaultNextApiRequest, 'query' | 'body'>; | |||||
export interface NextApiResponse { | export interface NextApiResponse { | ||||
body?: unknown; | body?: unknown; | ||||
} | } | ||||
export const ENCTYPE_APPLICATION_JSON = 'application/json' as const; | |||||
export const ENCTYPE_APPLICATION_OCTET_STREAM = 'application/octet-stream' as const; | |||||
export const ENCTYPE_MULTIPART_FORM_DATA = 'multipart/form-data' as const; | |||||
export const ENCTYPE_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded' as const; |
@@ -2,17 +2,29 @@ import { | |||||
GetServerSideProps, | GetServerSideProps, | ||||
NextApiHandler, | NextApiHandler, | ||||
NextApiRequest as DefaultNextApiRequest, | NextApiRequest as DefaultNextApiRequest, | ||||
NextApiResponse as DefaultNextApiResponse, PageConfig, | |||||
NextApiResponse as DefaultNextApiResponse, | |||||
PageConfig, | |||||
} from 'next'; | } from 'next'; | ||||
import * as nookies from 'nookies'; | import * as nookies from 'nookies'; | ||||
import {IncomingMessage} from 'http'; | |||||
import { IncomingMessage } from 'http'; | |||||
import busboy from 'busboy'; | import busboy from 'busboy'; | ||||
import {NextApiResponse, NextApiRequest} from './common'; | |||||
let BODY_COOKIE_KEY : string; | |||||
import { deserialize, serialize } from 'seroval'; | |||||
import { | |||||
NextApiResponse, | |||||
NextApiRequest, | |||||
ENCTYPE_APPLICATION_JSON, | |||||
ENCTYPE_X_WWW_FORM_URLENCODED, | |||||
ENCTYPE_MULTIPART_FORM_DATA, ENCTYPE_APPLICATION_OCTET_STREAM, | |||||
} from './common'; | |||||
let BODY_COOKIE_KEY: string; | |||||
let STATUS_CODE_COOKIE_KEY: string; | let STATUS_CODE_COOKIE_KEY: string; | ||||
let STATUS_MESSAGE_COOKIE_KEY: string; | let STATUS_MESSAGE_COOKIE_KEY: string; | ||||
let CONTENT_TYPE_COOKIE_KEY: string; | |||||
const generateContentTypeCookieKey = () => { | |||||
CONTENT_TYPE_COOKIE_KEY = `ifct${Date.now()}`; | |||||
}; | |||||
const generateBodyCookieKey = () => { | const generateBodyCookieKey = () => { | ||||
BODY_COOKIE_KEY = `ifb${Date.now()}`; | BODY_COOKIE_KEY = `ifb${Date.now()}`; | ||||
@@ -43,9 +55,12 @@ const getBody = (req: IncomingMessage) => new Promise<Buffer>((resolve, reject) | |||||
}); | }); | ||||
export namespace destination { | export namespace destination { | ||||
export const getServerSideProps = (gspFn?: GetServerSideProps): GetServerSideProps => async (ctx) => { | |||||
export const getServerSideProps = ( | |||||
gspFn?: GetServerSideProps, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const req: NextApiRequest = { | const req: NextApiRequest = { | ||||
query: ctx.query, | query: ctx.query, | ||||
body: null, | |||||
}; | }; | ||||
const { method = 'GET' } = ctx.req; | const { method = 'GET' } = ctx.req; | ||||
@@ -57,10 +72,12 @@ export namespace destination { | |||||
const cookies = nookies.parseCookies(ctx); | const cookies = nookies.parseCookies(ctx); | ||||
const res: NextApiResponse = {}; | const res: NextApiResponse = {}; | ||||
// TODO how to properly remove cookies without leftovers? | |||||
if (STATUS_CODE_COOKIE_KEY in cookies) { | if (STATUS_CODE_COOKIE_KEY in cookies) { | ||||
ctx.res.statusCode = Number(cookies[STATUS_CODE_COOKIE_KEY] || '200'); | ctx.res.statusCode = Number(cookies[STATUS_CODE_COOKIE_KEY] || '200'); | ||||
nookies.destroyCookie(ctx, STATUS_CODE_COOKIE_KEY, { | nookies.destroyCookie(ctx, STATUS_CODE_COOKIE_KEY, { | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | }); | ||||
} | } | ||||
@@ -68,26 +85,33 @@ export namespace destination { | |||||
ctx.res.statusMessage = cookies[STATUS_MESSAGE_COOKIE_KEY] || ''; | ctx.res.statusMessage = cookies[STATUS_MESSAGE_COOKIE_KEY] || ''; | ||||
nookies.destroyCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, { | nookies.destroyCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, { | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | }); | ||||
} | } | ||||
if (BODY_COOKIE_KEY in cookies) { | if (BODY_COOKIE_KEY in cookies) { | ||||
const resBody = cookies[BODY_COOKIE_KEY]; | const resBody = cookies[BODY_COOKIE_KEY]; | ||||
if (resBody.startsWith('json:')) { | |||||
res.body = JSON.parse(resBody.slice(5)); | |||||
} else if (resBody.startsWith('raw:')) { | |||||
res.body = resBody.slice(4); | |||||
if (cookies[CONTENT_TYPE_COOKIE_KEY] === ENCTYPE_APPLICATION_JSON) { | |||||
res.body = deserialize(resBody); | |||||
} else if (cookies[CONTENT_TYPE_COOKIE_KEY] === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||||
res.body = deserialize(resBody); | |||||
} else { | } else { | ||||
console.warn('Could not parse response body, returning nothing'); | |||||
const c = console; | |||||
c.warn('Could not parse response body, returning nothing'); | |||||
} | } | ||||
nookies.destroyCookie(ctx, BODY_COOKIE_KEY, { | nookies.destroyCookie(ctx, BODY_COOKIE_KEY, { | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | |||||
nookies.destroyCookie(ctx, CONTENT_TYPE_COOKIE_KEY, { | |||||
path: '/', | |||||
httpOnly: true, | |||||
}); | }); | ||||
} | } | ||||
let gspResult; | let gspResult; | ||||
if (gspFn) { | if (gspFn) { | ||||
gspResult = await gspFn(ctx) | |||||
gspResult = await gspFn(ctx); | |||||
} else { | } else { | ||||
gspResult = { | gspResult = { | ||||
props: {}, | props: {}, | ||||
@@ -110,84 +134,82 @@ export namespace destination { | |||||
} | } | ||||
export namespace action { | export namespace action { | ||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
export const wrapApiHandler = (fn: NextApiHandler): NextApiHandler => async (req, res) => { | |||||
const body = await deserializeBody(req); | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
reqMut.body = body; | |||||
return fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; | |||||
const parseMultipartFormData = async (req: IncomingMessage) => { | |||||
return new Promise<Record<string, unknown>>((resolve, reject) => { | |||||
const body: Record<string, unknown> = {}; | |||||
const bb = busboy({ | |||||
headers: req.headers, | |||||
}); | |||||
bb.on('file', (name, file, info) => { | |||||
const { | |||||
filename, | |||||
mimeType: mimetype | |||||
} = info; | |||||
const parseMultipartFormData = async ( | |||||
req: IncomingMessage, | |||||
) => new Promise<Record<string, unknown>>((resolve, reject) => { | |||||
const body: Record<string, unknown> = {}; | |||||
const bb = busboy({ | |||||
headers: req.headers, | |||||
}); | |||||
let fileData = Buffer.from(''); | |||||
bb.on('file', (name, file, info) => { | |||||
const { | |||||
filename, | |||||
mimeType: mimetype, | |||||
} = info; | |||||
file.on('data', (data) => { | |||||
fileData = Buffer.concat([fileData, data]); | |||||
}); | |||||
let fileData = Buffer.from(''); | |||||
file.on('close', () => { | |||||
body[name] = new File([fileData.buffer], filename, { | |||||
type: mimetype, | |||||
}); | |||||
}); | |||||
file.on('data', (data) => { | |||||
fileData = Buffer.concat([fileData, data]); | |||||
}); | }); | ||||
bb.on('field', (name, value) => { | |||||
body[name] = value; | |||||
file.on('close', () => { | |||||
body[name] = new File([fileData.buffer], filename, { | |||||
type: mimetype, | |||||
}); | |||||
}); | }); | ||||
}); | |||||
bb.on('close', () => { | |||||
resolve(body); | |||||
}); | |||||
bb.on('field', (name, value) => { | |||||
body[name] = value; | |||||
}); | |||||
bb.on('error', (error) => { | |||||
reject(error); | |||||
}); | |||||
bb.on('close', () => { | |||||
resolve(body); | |||||
}); | |||||
req.pipe(bb); | |||||
bb.on('error', (error) => { | |||||
reject(error); | |||||
}); | }); | ||||
}; | |||||
req.pipe(bb); | |||||
}); | |||||
const deserializeBody = async (req: IncomingMessage) => { | const deserializeBody = async (req: IncomingMessage) => { | ||||
const contentType = req.headers['content-type']; | const contentType = req.headers['content-type']; | ||||
// TODO get body encoding from headers | |||||
const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding; | const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding; | ||||
// should we turn off default body parsing? | |||||
if (contentType === 'application/json') { | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||||
const bodyRaw = await getBody(req); | const bodyRaw = await getBody(req); | ||||
return JSON.parse(bodyRaw.toString(encoding)); | |||||
return JSON.parse(bodyRaw.toString(encoding)) as Record<string, unknown>; | |||||
} | } | ||||
if (contentType === 'application/x-www-form-urlencoded') { | |||||
if (contentType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||||
const bodyRaw = await getBody(req); | const bodyRaw = await getBody(req); | ||||
return Object.fromEntries( | return Object.fromEntries( | ||||
new URLSearchParams(bodyRaw.toString(encoding)).entries() | |||||
new URLSearchParams(bodyRaw.toString(encoding)).entries(), | |||||
); | ); | ||||
} | } | ||||
if (contentType?.startsWith('multipart/form-data;')) { | |||||
if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { | |||||
return parseMultipartFormData(req); | return parseMultipartFormData(req); | ||||
} | } | ||||
const bodyRaw = await getBody(req); | const bodyRaw = await getBody(req); | ||||
return bodyRaw.toString('binary'); | return bodyRaw.toString('binary'); | ||||
}; | }; | ||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
export const wrapApiHandler = (fn: NextApiHandler): NextApiHandler => async (req, res) => { | |||||
const body = await deserializeBody(req); | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
reqMut.body = body; | |||||
return fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; | |||||
export const getServerSideProps = (fn: NextApiHandler): GetServerSideProps => async (ctx) => { | export const getServerSideProps = (fn: NextApiHandler): GetServerSideProps => async (ctx) => { | ||||
const { referer } = ctx.req.headers; | const { referer } = ctx.req.headers; | ||||
@@ -196,7 +218,8 @@ export namespace action { | |||||
body: await deserializeBody(ctx.req), | body: await deserializeBody(ctx.req), | ||||
} as DefaultNextApiRequest; | } as DefaultNextApiRequest; | ||||
let data = null; | |||||
let data: unknown = null; | |||||
let contentType: string | undefined; | |||||
const mockRes = { | const mockRes = { | ||||
// todo handle other nextapiresponse methods (e.g. setting headers, writeHead, etc.) | // todo handle other nextapiresponse methods (e.g. setting headers, writeHead, etc.) | ||||
statusMessage: '', | statusMessage: '', | ||||
@@ -206,12 +229,21 @@ export namespace action { | |||||
this.statusCode = code; | this.statusCode = code; | ||||
return mockRes; | return mockRes; | ||||
}, | }, | ||||
send: (raw: any) => { | |||||
send: (raw?: unknown) => { | |||||
// todo: how to transfer binary response in a more compact way? | // todo: how to transfer binary response in a more compact way? | ||||
data = `raw:${raw.toString('base64')}`; | |||||
if (typeof raw === 'undefined' || raw === null) { | |||||
return; | |||||
} | |||||
if (raw instanceof Buffer) { | |||||
contentType = ENCTYPE_APPLICATION_OCTET_STREAM; | |||||
} | |||||
data = serialize(raw); | |||||
}, | }, | ||||
json: (raw: any) => { | |||||
data = `json:${JSON.stringify(raw)}`; | |||||
json: (raw: unknown) => { | |||||
contentType = ENCTYPE_APPLICATION_JSON; | |||||
data = serialize(raw); | |||||
}, | }, | ||||
} as DefaultNextApiResponse; | } as DefaultNextApiResponse; | ||||
@@ -221,20 +253,32 @@ export namespace action { | |||||
nookies.setCookie(ctx, STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString(), { | nookies.setCookie(ctx, STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString(), { | ||||
maxAge: 30 * 24 * 60 * 60, | maxAge: 30 * 24 * 60 * 60, | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | }); | ||||
generateStatusMessageCookieKey(); | generateStatusMessageCookieKey(); | ||||
nookies.setCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage, { | nookies.setCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage, { | ||||
maxAge: 30 * 24 * 60 * 60, | maxAge: 30 * 24 * 60 * 60, | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | }); | ||||
if (data) { | if (data) { | ||||
generateBodyCookieKey(); | generateBodyCookieKey(); | ||||
nookies.setCookie(ctx, BODY_COOKIE_KEY, data, { | |||||
nookies.setCookie(ctx, BODY_COOKIE_KEY, data as string, { | |||||
maxAge: 30 * 24 * 60 * 60, | maxAge: 30 * 24 * 60 * 60, | ||||
path: '/', | path: '/', | ||||
httpOnly: true, | |||||
}); | }); | ||||
if (contentType) { | |||||
generateContentTypeCookieKey(); | |||||
nookies.setCookie(ctx, CONTENT_TYPE_COOKIE_KEY, contentType, { | |||||
maxAge: 30 * 24 * 60 * 60, | |||||
path: '/', | |||||
httpOnly: true, | |||||
}); | |||||
} | |||||
} | } | ||||
return { | return { | ||||
@@ -20,6 +20,9 @@ importers: | |||||
nookies: | nookies: | ||||
specifier: ^2.5.2 | specifier: ^2.5.2 | ||||
version: 2.5.2 | version: 2.5.2 | ||||
seroval: | |||||
specifier: ^0.9.0 | |||||
version: 0.9.0 | |||||
devDependencies: | devDependencies: | ||||
'@testing-library/jest-dom': | '@testing-library/jest-dom': | ||||
specifier: ^5.16.5 | specifier: ^5.16.5 | ||||
@@ -4774,6 +4777,11 @@ packages: | |||||
- supports-color | - supports-color | ||||
dev: true | dev: true | ||||
/seroval@0.9.0: | |||||
resolution: {integrity: sha512-Ttr96/8czi3SXjbFFzpRc2Xpp1wvBufmaNuTviUL8eGQhUr1mdeiQ6YYSaLnMwMc4YWSeBggq72bKEBVu6/IFA==} | |||||
engines: {node: '>=10'} | |||||
dev: false | |||||
/serve-static@1.15.0: | /serve-static@1.15.0: | ||||
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} | resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} | ||||
engines: {node: '>= 0.8.0'} | engines: {node: '>= 0.8.0'} | ||||