import { IncomingMessage } from 'http'; import { getFormValues } from '@theoryofnekomata/formxtra'; import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM, ENCTYPE_MULTIPART_FORM_DATA, ENCTYPE_X_WWW_FORM_URLENCODED, } from '../common/enctypes'; import { getBody, parseMultipartFormData } from './request'; import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY, CONTENT_ENCODING_HEADER_KEY, CONTENT_TYPE_HEADER_KEY, DEFAULT_ENCODING, } from '../common/constants'; export type EncTypeSerializer = (data: unknown) => string; export type EncTypeSerializerMap = Record; export type SerializerOptions = Parameters[1]; export interface SerializeBodyParams { form: HTMLFormElement, encType: string, serializers?: EncTypeSerializerMap, serializerOptions?: SerializerOptions, methodFormKey?: string, preventRedirectFormKey?: string, } export const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { [ENCTYPE_APPLICATION_JSON]: (data: unknown) => JSON.stringify(data), }; export const serializeBody = (params: SerializeBodyParams) => { const { form, encType, serializers = DEFAULT_ENCTYPE_SERIALIZERS, serializerOptions, methodFormKey = METHOD_FORM_KEY, preventRedirectFormKey = PREVENT_REDIRECT_FORM_KEY, } = params; if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { const searchParams = new URLSearchParams(form); searchParams.delete(methodFormKey); searchParams.delete(preventRedirectFormKey); return searchParams; } if (encType === ENCTYPE_MULTIPART_FORM_DATA) { // type error when provided a submitter element for some reason... const FormDataUnknown = FormData as unknown as { new(formElement?: HTMLElement, submitter?: HTMLElement): FormData & { entries: () => IterableIterator<[string, unknown]>; }; }; const formData = new FormDataUnknown(form, serializerOptions?.submitter); const emptyFiles = Array.from(formData.entries()) .filter(([, value]) => ( value instanceof File && value.size === 0 && value.name === '' )); emptyFiles.forEach(([key]) => formData.delete(key)); formData.delete(methodFormKey); formData.delete(preventRedirectFormKey); return formData; } if (typeof serializers[encType] === 'function') { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars [methodFormKey]: _method, // eslint-disable-next-line @typescript-eslint/no-unused-vars [preventRedirectFormKey]: _preventRedirect, ...formValues } = getFormValues(form, serializerOptions); return serializers[encType](formValues); } throw new Error(`Unsupported encType: ${encType}`); }; export type EncTypeDeserializer = (data: string) => unknown; export type EncTypeDeserializerMap = Record; export interface DeserializeBodyParams { req: IncomingMessage, deserializers?: EncTypeDeserializerMap, } export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { [ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record, [ENCTYPE_X_WWW_FORM_URLENCODED]: (data: string) => Object.fromEntries( new URLSearchParams(data).entries(), ), }; export const deserializeFormObjectBody = async (params: DeserializeBodyParams) => { const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; const contentType = req.headers[CONTENT_TYPE_HEADER_KEY] ?? ENCTYPE_APPLICATION_OCTET_STREAM; if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { return parseMultipartFormData(req); } const bodyRaw = await getBody(req); const encoding = ( req.headers[CONTENT_ENCODING_HEADER_KEY] ?? DEFAULT_ENCODING ) as BufferEncoding; const { [contentType]: theDeserializer } = deserializers; if (typeof theDeserializer !== 'function') { throw new Error(`Could not deserialize body with content type: ${contentType}`); } return theDeserializer(bodyRaw.toString(encoding)) as Record; };