Allow ways to send methods other than get/post on server-side.master
@@ -6,6 +6,18 @@ const ActionNotesResourcePage: NextPage = () => null; | |||||
const getServerSideProps = Iceform.action.getServerSideProps({ | const getServerSideProps = Iceform.action.getServerSideProps({ | ||||
fn: noteResource, | fn: noteResource, | ||||
onAction: async (context) => { | |||||
if (context.req.method?.toLowerCase() === 'delete') { | |||||
return { | |||||
redirect: { | |||||
destination: '/notes', | |||||
permanent: false, | |||||
}, | |||||
}; | |||||
} | |||||
// use default behavior | |||||
}, | |||||
}); | }); | ||||
export { | export { | ||||
@@ -1,11 +1,21 @@ | |||||
import * as Iceform from '@modal-sh/iceform-next'; | import * as Iceform from '@modal-sh/iceform-next'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
const NotesItemPage: Iceform.NextPage = ({ | |||||
export interface NotesItemPageProps { | |||||
note: { | |||||
id: string; | |||||
title: string; | |||||
content: string; | |||||
image: string; | |||||
} | |||||
} | |||||
const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({ | |||||
req, | req, | ||||
res, | res, | ||||
note, | |||||
}) => { | }) => { | ||||
const body = (res.body ?? {}) as Record<string, unknown>; | |||||
const body = (res.body ?? note ?? {}) as Record<string, unknown>; | |||||
const {response, loading, ...isoformProps} = Iceform.useResponse({ | const {response, loading, ...isoformProps} = Iceform.useResponse({ | ||||
res | res | ||||
}); | }); | ||||
@@ -26,22 +36,23 @@ const NotesItemPage: Iceform.NextPage = ({ | |||||
method="post" | method="post" | ||||
action={`/a/notes/${req.query.noteId}`} | action={`/a/notes/${req.query.noteId}`} | ||||
clientAction={`/api/notes/${req.query.noteId}`} | clientAction={`/api/notes/${req.query.noteId}`} | ||||
clientMethod="put" | |||||
> | > | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Title</span> | |||||
<span className="after:block">Title</span> | |||||
<input type="text" name="title" defaultValue={body.title as string} /> | <input type="text" name="title" defaultValue={body.title as string} /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Image</span> | |||||
<span className="after:block">Image</span> | |||||
<input type="file" name="image" /> | <input type="file" name="image" /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Content</span> | |||||
<span className="after:block">Content</span> | |||||
<textarea name="content" defaultValue={body.content as string} /> | <textarea name="content" defaultValue={body.content as string} /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
@@ -49,9 +60,40 @@ const NotesItemPage: Iceform.NextPage = ({ | |||||
<button type="submit">Submit</button> | <button type="submit">Submit</button> | ||||
</div> | </div> | ||||
</Iceform.Form> | </Iceform.Form> | ||||
) | |||||
); | |||||
}; | }; | ||||
export const getServerSideProps = Iceform.destination.getServerSideProps(); | |||||
export const getServerSideProps = Iceform.destination.getServerSideProps({ | |||||
fn: async (actionReq, actionRes, ctx) => { | |||||
const {noteId} = ctx.query; | |||||
let origin: string; | |||||
if (ctx.req.headers.referer) { | |||||
const refererUrl = new URL(ctx.req.headers.referer as string); | |||||
origin = refererUrl.origin; | |||||
} else { | |||||
// TODO how to get the scheme? | |||||
const scheme = 'http'; | |||||
origin = `${scheme}://${ctx.req.headers.host}`; | |||||
} | |||||
const url = new URL(`/api/notes/${noteId}`, origin); | |||||
const noteResponse = await fetch(url.toString(), { | |||||
headers: { | |||||
'Accept': 'application/json', | |||||
} | |||||
}); | |||||
if (noteResponse.ok) { | |||||
const note = await noteResponse.json(); | |||||
return { | |||||
props: { | |||||
note, | |||||
}, | |||||
}; | |||||
} | |||||
return { | |||||
notFound: true, | |||||
}; | |||||
}, | |||||
}); | |||||
export default NotesItemPage; | export default NotesItemPage; |
@@ -10,19 +10,19 @@ const NotesPage: NextPage = () => { | |||||
> | > | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Title</span> | |||||
<span className="after:block">Title</span> | |||||
<input type="text" name="title" /> | <input type="text" name="title" /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Image</span> | |||||
<span className="after:block">Image</span> | |||||
<input type="file" name="image" /> | <input type="file" name="image" /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
<label> | <label> | ||||
<span>Content</span> | |||||
<span className="after:block">Content</span> | |||||
<textarea name="content" /> | <textarea name="content" /> | ||||
</label> | </label> | ||||
</div> | </div> | ||||
@@ -25,3 +25,7 @@ body { | |||||
) | ) | ||||
rgb(var(--background-start-rgb)); | rgb(var(--background-start-rgb)); | ||||
} | } | ||||
input, select, textarea { | |||||
background-color: rgb(var(--background-start-rgb)); | |||||
} |
@@ -105,3 +105,4 @@ dist | |||||
.tern-port | .tern-port | ||||
.npmrc | .npmrc | ||||
types/ |
@@ -15,6 +15,7 @@ import { | |||||
FormDerivedElementComponent, | FormDerivedElementComponent, | ||||
} from '../common'; | } from '../common'; | ||||
import { useFormFetch } from '../hooks/useFormFetch'; | import { useFormFetch } from '../hooks/useFormFetch'; | ||||
import { METHOD_FORM_KEY } from '../../common/constants'; | |||||
export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'action' | 'method'> { | export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'action' | 'method'> { | ||||
action?: string; | action?: string; | ||||
@@ -59,9 +60,16 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
responseEncType, | responseEncType, | ||||
serializerOptions, | serializerOptions, | ||||
}); | }); | ||||
const serverMethodRaw = method.toLowerCase(); | const serverMethodRaw = method.toLowerCase(); | ||||
const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; | const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; | ||||
const [serverMethodOverride, setServerMethodOverride] = React.useState( | |||||
serverMethod.toLowerCase() !== clientMethod.toLowerCase(), | |||||
); | |||||
React.useEffect(() => { | |||||
// hide server override in client | |||||
setServerMethodOverride(false); | |||||
}, []); | |||||
return ( | return ( | ||||
<FormDerivedElementComponent | <FormDerivedElementComponent | ||||
@@ -72,6 +80,9 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||||
method={serverMethod} | method={serverMethod} | ||||
encType={ENCTYPE_MULTIPART_FORM_DATA} | encType={ENCTYPE_MULTIPART_FORM_DATA} | ||||
> | > | ||||
{serverMethodOverride && ( | |||||
<input type="hidden" name={METHOD_FORM_KEY} value={clientMethod} /> | |||||
)} | |||||
{children} | {children} | ||||
</FormDerivedElementComponent> | </FormDerivedElementComponent> | ||||
); | ); | ||||
@@ -0,0 +1,2 @@ | |||||
export const PREVENT_REDIRECT_FORM_KEY = '__iceform_prevent_redirect' as const; | |||||
export const METHOD_FORM_KEY = '__iceform_method' as const; |
@@ -0,0 +1,141 @@ | |||||
import { | |||||
GetServerSideProps, | |||||
NextApiHandler, | |||||
PageConfig, | |||||
NextApiRequest as DefaultNextApiRequest, PreviewData, GetServerSidePropsContext, | |||||
} from 'next'; | |||||
import { ParsedUrlQuery } from 'querystring'; | |||||
import { deserializeBody, EncTypeDeserializerMap } from '../utils/serialization'; | |||||
import { IceformNextServerResponse } from './response'; | |||||
import { | |||||
BODY_COOKIE_KEY, CONTENT_TYPE_COOKIE_KEY, | |||||
CookieManager, | |||||
STATUS_CODE_COOKIE_KEY, | |||||
STATUS_MESSAGE_COOKIE_KEY, | |||||
} from '../utils/cookies'; | |||||
import { | |||||
METHOD_FORM_KEY, | |||||
PREVENT_REDIRECT_FORM_KEY, | |||||
} from '../common/constants'; | |||||
import { | |||||
ACTION_STATUS_CODE, | |||||
DEFAULT_METHOD, | |||||
} from './constants'; | |||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
export type OnActionFunction< | |||||
Props extends Record<string, unknown> = Record<string, unknown>, | |||||
Params extends ParsedUrlQuery = ParsedUrlQuery, | |||||
Preview extends PreviewData = PreviewData, | |||||
> = ( | |||||
context: GetServerSidePropsContext<Params, Preview>, | |||||
) => Promise<Awaited<ReturnType<GetServerSideProps<Props, Params, Preview>>> | undefined>; | |||||
export interface ActionWrapperOptions { | |||||
fn: NextApiHandler, | |||||
onAction?: OnActionFunction, | |||||
deserializers?: EncTypeDeserializerMap, | |||||
/** | |||||
* Maps the Location header from the handler response to an accessible URL. | |||||
* @param url | |||||
*/ | |||||
mapLocationToRedirectDestination?: (referer: string, url: string) => string, | |||||
} | |||||
export const wrapApiHandler = ( | |||||
options: ActionWrapperOptions, | |||||
): NextApiHandler => async (req, res) => { | |||||
const body = await deserializeBody({ | |||||
req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
reqMut.body = body; | |||||
return options.fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; | |||||
export const getServerSideProps = ( | |||||
options: ActionWrapperOptions, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const { referer = '/' } = ctx.req.headers; | |||||
const deserialized = await deserializeBody({ | |||||
req: ctx.req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
const defaultMethod = ctx.req.method ?? DEFAULT_METHOD; | |||||
let effectiveMethod = defaultMethod; | |||||
let mockReqBody: unknown = deserialized; | |||||
if (typeof deserialized === 'object' && deserialized !== null) { | |||||
const { | |||||
[METHOD_FORM_KEY]: theMethod, | |||||
...theMockReqBody | |||||
} = deserialized as { | |||||
[METHOD_FORM_KEY]?: string, | |||||
[key: string]: unknown, | |||||
}; | |||||
effectiveMethod = theMethod ?? defaultMethod; | |||||
mockReqBody = theMockReqBody; | |||||
} | |||||
const mockReq = { | |||||
...ctx.req, | |||||
body: mockReqBody, | |||||
query: { | |||||
...ctx.query, | |||||
...(ctx.params ?? {}), | |||||
}, | |||||
// ?: how to prevent malicious method spoofing? | |||||
method: effectiveMethod, | |||||
} as DefaultNextApiRequest; | |||||
const mockRes = new IceformNextServerResponse(ctx.req); | |||||
await options.fn(mockReq, mockRes); | |||||
const cookieManager = new CookieManager(ctx); | |||||
cookieManager.setCookie(STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString()); | |||||
cookieManager.setCookie(STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage); | |||||
if (mockRes.data) { | |||||
cookieManager.setCookie(BODY_COOKIE_KEY, mockRes.data as string); | |||||
if (mockRes.contentType) { | |||||
cookieManager.setCookie(CONTENT_TYPE_COOKIE_KEY, mockRes.contentType); | |||||
} | |||||
} | |||||
if (typeof options.onAction === 'function') { | |||||
const onActionResult = await options.onAction(ctx); | |||||
if (onActionResult) { | |||||
return onActionResult; | |||||
} | |||||
} | |||||
const preventRedirect = ( | |||||
typeof mockReq.body === 'object' | |||||
&& mockReq.body !== null | |||||
&& PREVENT_REDIRECT_FORM_KEY in mockReq.body | |||||
); | |||||
const redirectDestination = ( | |||||
mockRes.location | |||||
&& typeof options.mapLocationToRedirectDestination === 'function' | |||||
&& !preventRedirect | |||||
) | |||||
? options.mapLocationToRedirectDestination(referer, mockRes.location) | |||||
: referer; | |||||
return { | |||||
redirect: { | |||||
destination: redirectDestination, | |||||
statusCode: ACTION_STATUS_CODE, | |||||
}, | |||||
props: { | |||||
query: ctx.query, | |||||
body: mockRes.data, | |||||
}, | |||||
}; | |||||
}; |
@@ -1,5 +1,4 @@ | |||||
export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'] as const; | export const METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH', 'DELETE'] as const; | ||||
export const PREVENT_REDIRECT_FORM_KEY = '__iceform_prevent_redirect' as const; | |||||
export const DEFAULT_METHOD = 'GET' as const; | export const DEFAULT_METHOD = 'GET' as const; | ||||
export const DEFAULT_ENCODING = 'utf-8' as const; | export const DEFAULT_ENCODING = 'utf-8' as const; | ||||
export const ACTION_STATUS_CODE = 307 as const; // temporary redirect | export const ACTION_STATUS_CODE = 307 as const; // temporary redirect | ||||
@@ -0,0 +1,103 @@ | |||||
import { ParsedUrlQuery } from 'querystring'; | |||||
import { GetServerSideProps, GetServerSidePropsContext, PreviewData } from 'next'; | |||||
import { deserialize } from 'seroval'; | |||||
import { NextApiRequest, NextApiResponse } from '../common/types'; | |||||
import { | |||||
DEFAULT_ENCODING, | |||||
DEFAULT_METHOD, | |||||
DEFAULT_RESPONSE_STATUS_CODE, | |||||
METHODS_WITH_BODY, | |||||
} from './constants'; | |||||
import { getBody } from '../utils/request'; | |||||
import { | |||||
BODY_COOKIE_KEY, CONTENT_TYPE_COOKIE_KEY, | |||||
CookieManager, | |||||
STATUS_CODE_COOKIE_KEY, | |||||
STATUS_MESSAGE_COOKIE_KEY, | |||||
} from '../utils/cookies'; | |||||
import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | |||||
export type DestinationGetServerSideProps< | |||||
Props extends Record<string, unknown> = Record<string, unknown>, | |||||
Params extends ParsedUrlQuery = ParsedUrlQuery, | |||||
Preview extends PreviewData = PreviewData, | |||||
> = ( | |||||
actionReq: NextApiRequest, | |||||
actionRes: NextApiResponse, | |||||
context: GetServerSidePropsContext<Params, Preview>, | |||||
) => ReturnType<GetServerSideProps<Props, Params, Preview>>; | |||||
export interface DestinationWrapperOptions { | |||||
fn?: DestinationGetServerSideProps; | |||||
} | |||||
export const getServerSideProps = ( | |||||
options = {} as DestinationWrapperOptions, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const req: NextApiRequest = { | |||||
query: { | |||||
...ctx.query, | |||||
...(ctx.params ?? {}), | |||||
}, | |||||
body: null, | |||||
}; | |||||
const { method = DEFAULT_METHOD } = ctx.req; | |||||
if (METHODS_WITH_BODY.includes(method.toUpperCase() as typeof METHODS_WITH_BODY[number])) { | |||||
const body = await getBody(ctx.req); | |||||
req.body = body.toString(DEFAULT_ENCODING); | |||||
} | |||||
const cookieManager = new CookieManager(ctx); | |||||
// TODO how to properly remove cookies without leftovers? | |||||
if (cookieManager.hasCookie(STATUS_CODE_COOKIE_KEY)) { | |||||
ctx.res.statusCode = Number( | |||||
cookieManager.getCookie(STATUS_CODE_COOKIE_KEY) || DEFAULT_RESPONSE_STATUS_CODE, | |||||
); | |||||
cookieManager.unsetCookie(STATUS_CODE_COOKIE_KEY); | |||||
} | |||||
if (cookieManager.hasCookie(STATUS_MESSAGE_COOKIE_KEY)) { | |||||
ctx.res.statusMessage = cookieManager.getCookie(STATUS_MESSAGE_COOKIE_KEY) || ''; | |||||
cookieManager.unsetCookie(STATUS_MESSAGE_COOKIE_KEY); | |||||
} | |||||
const res: NextApiResponse = {}; | |||||
if (cookieManager.hasCookie(BODY_COOKIE_KEY)) { | |||||
const resBody = cookieManager.getCookie(BODY_COOKIE_KEY); | |||||
const contentType = cookieManager.getCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||||
res.body = deserialize(resBody); | |||||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||||
res.body = deserialize(resBody); | |||||
} else { | |||||
const c = console; | |||||
c.warn('Could not parse response body, returning nothing'); | |||||
res.body = null; | |||||
} | |||||
cookieManager.unsetCookie(BODY_COOKIE_KEY); | |||||
cookieManager.unsetCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
} | |||||
const gspResult = ( | |||||
typeof options?.fn === 'function' | |||||
? await options.fn(req, res, ctx) | |||||
: { | |||||
props: {}, | |||||
} | |||||
); | |||||
if ('props' in gspResult) { | |||||
return { | |||||
...gspResult, | |||||
props: { | |||||
...gspResult.props, | |||||
req, | |||||
res, | |||||
}, | |||||
}; | |||||
} | |||||
// redirect/not found will be treated as default behavior | |||||
return gspResult; | |||||
}; |
@@ -1,187 +1,2 @@ | |||||
import { | |||||
GetServerSideProps, | |||||
NextApiHandler, | |||||
NextApiRequest as DefaultNextApiRequest, | |||||
PageConfig, | |||||
} from 'next'; | |||||
import { deserialize } from 'seroval'; | |||||
import { | |||||
NextApiResponse, | |||||
NextApiRequest, | |||||
} from '../common/types'; | |||||
import { | |||||
ENCTYPE_APPLICATION_JSON, | |||||
ENCTYPE_APPLICATION_OCTET_STREAM, | |||||
} from '../common/enctypes'; | |||||
import { getBody } from '../utils/request'; | |||||
import { | |||||
CookieManager, | |||||
BODY_COOKIE_KEY, | |||||
STATUS_CODE_COOKIE_KEY, | |||||
STATUS_MESSAGE_COOKIE_KEY, | |||||
CONTENT_TYPE_COOKIE_KEY, | |||||
} from '../utils/cookies'; | |||||
import { deserializeBody, EncTypeDeserializerMap } from '../utils/serialization'; | |||||
import { IceformNextServerResponse } from './response'; | |||||
import { | |||||
DEFAULT_METHOD, | |||||
DEFAULT_ENCODING, | |||||
DEFAULT_RESPONSE_STATUS_CODE, | |||||
ACTION_STATUS_CODE, | |||||
METHODS_WITH_BODY, | |||||
PREVENT_REDIRECT_FORM_KEY, | |||||
} from './constants'; | |||||
export namespace destination { | |||||
export const getServerSideProps = ( | |||||
gspFn?: GetServerSideProps, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const req: NextApiRequest = { | |||||
query: { | |||||
...ctx.query, | |||||
...(ctx.params ?? {}), | |||||
}, | |||||
body: null, | |||||
}; | |||||
const { method = DEFAULT_METHOD } = ctx.req; | |||||
if (METHODS_WITH_BODY.includes(method.toUpperCase() as typeof METHODS_WITH_BODY[number])) { | |||||
const body = await getBody(ctx.req); | |||||
req.body = body.toString(DEFAULT_ENCODING); | |||||
} | |||||
const cookieManager = new CookieManager(ctx); | |||||
// TODO how to properly remove cookies without leftovers? | |||||
if (cookieManager.hasCookie(STATUS_CODE_COOKIE_KEY)) { | |||||
ctx.res.statusCode = Number( | |||||
cookieManager.getCookie(STATUS_CODE_COOKIE_KEY) || DEFAULT_RESPONSE_STATUS_CODE, | |||||
); | |||||
cookieManager.unsetCookie(STATUS_CODE_COOKIE_KEY); | |||||
} | |||||
if (cookieManager.hasCookie(STATUS_MESSAGE_COOKIE_KEY)) { | |||||
ctx.res.statusMessage = cookieManager.getCookie(STATUS_MESSAGE_COOKIE_KEY) || ''; | |||||
cookieManager.unsetCookie(STATUS_MESSAGE_COOKIE_KEY); | |||||
} | |||||
const res: NextApiResponse = {}; | |||||
if (cookieManager.hasCookie(BODY_COOKIE_KEY)) { | |||||
const resBody = cookieManager.getCookie(BODY_COOKIE_KEY); | |||||
const contentType = cookieManager.getCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||||
res.body = deserialize(resBody); | |||||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||||
res.body = deserialize(resBody); | |||||
} else { | |||||
const c = console; | |||||
c.warn('Could not parse response body, returning nothing'); | |||||
res.body = null; | |||||
} | |||||
cookieManager.unsetCookie(BODY_COOKIE_KEY); | |||||
cookieManager.unsetCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
} | |||||
let gspResult; | |||||
if (gspFn) { | |||||
gspResult = await gspFn(ctx); | |||||
} else { | |||||
gspResult = { | |||||
props: {}, | |||||
}; | |||||
} | |||||
if ('props' in gspResult) { | |||||
return { | |||||
...gspResult, | |||||
props: { | |||||
...gspResult.props, | |||||
req, | |||||
res, | |||||
}, | |||||
}; | |||||
} | |||||
// redirect/not found will be treated as default behavior | |||||
return gspResult; | |||||
}; | |||||
} | |||||
export namespace action { | |||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
export interface ActionWrapperOptions { | |||||
fn: NextApiHandler, | |||||
deserializers?: EncTypeDeserializerMap, | |||||
/** | |||||
* Maps the Location header from the handler response to an accessible URL. | |||||
* @param url | |||||
*/ | |||||
mapLocationToRedirectDestination?: (referer: string, url: string) => string, | |||||
} | |||||
export const wrapApiHandler = ( | |||||
options: ActionWrapperOptions, | |||||
): NextApiHandler => async (req, res) => { | |||||
const body = await deserializeBody({ | |||||
req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
reqMut.body = body; | |||||
return options.fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; | |||||
export const getServerSideProps = ( | |||||
options: ActionWrapperOptions, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const { referer = '/' } = ctx.req.headers; | |||||
const mockReq = { | |||||
...ctx.req, | |||||
body: await deserializeBody({ | |||||
req: ctx.req, | |||||
deserializers: options.deserializers, | |||||
}), | |||||
} as DefaultNextApiRequest; | |||||
const mockRes = new IceformNextServerResponse(ctx.req); | |||||
await options.fn(mockReq, mockRes); | |||||
const cookieManager = new CookieManager(ctx); | |||||
cookieManager.setCookie(STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString()); | |||||
cookieManager.setCookie(STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage); | |||||
if (mockRes.data) { | |||||
cookieManager.setCookie(BODY_COOKIE_KEY, mockRes.data as string); | |||||
if (mockRes.contentType) { | |||||
cookieManager.setCookie(CONTENT_TYPE_COOKIE_KEY, mockRes.contentType); | |||||
} | |||||
} | |||||
const preventRedirect = ( | |||||
typeof mockReq.body === 'object' | |||||
&& mockReq.body !== null | |||||
&& PREVENT_REDIRECT_FORM_KEY in mockReq.body | |||||
); | |||||
const redirectDestination = ( | |||||
mockRes.location | |||||
&& typeof options.mapLocationToRedirectDestination === 'function' | |||||
&& !preventRedirect | |||||
) | |||||
? options.mapLocationToRedirectDestination(referer, mockRes.location) | |||||
: referer; | |||||
return { | |||||
redirect: { | |||||
destination: redirectDestination, | |||||
statusCode: ACTION_STATUS_CODE, | |||||
}, | |||||
props: { | |||||
query: ctx.query, | |||||
body: mockRes.data, | |||||
}, | |||||
}; | |||||
}; | |||||
} | |||||
export * as action from './action'; | |||||
export * as destination from './destination'; |
@@ -64,7 +64,7 @@ export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { | |||||
[ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>, | [ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>, | ||||
}; | }; | ||||
export const deserializeBody = async (params: DeserializeBodyParams) => { | |||||
export const deserializeBody = async (params: DeserializeBodyParams): Promise<unknown> => { | |||||
const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | ||||
const contentType = req.headers['content-type'] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | const contentType = req.headers['content-type'] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | ||||