Use forms with or without client-side JavaScript--no code duplication required!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

123 lines
3.9 KiB

  1. import { IncomingMessage } from 'http';
  2. import { getFormValues } from '@theoryofnekomata/formxtra';
  3. import {
  4. ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM,
  5. ENCTYPE_MULTIPART_FORM_DATA,
  6. ENCTYPE_X_WWW_FORM_URLENCODED,
  7. } from '../common/enctypes';
  8. import { getBody, parseMultipartFormData } from './request';
  9. import {
  10. METHOD_FORM_KEY,
  11. PREVENT_REDIRECT_FORM_KEY,
  12. CONTENT_ENCODING_HEADER_KEY,
  13. CONTENT_TYPE_HEADER_KEY,
  14. DEFAULT_ENCODING,
  15. } from '../common/constants';
  16. export type EncTypeSerializer = (data: unknown) => string;
  17. export type EncTypeSerializerMap = Record<string, EncTypeSerializer>;
  18. export type SerializerOptions = Parameters<typeof getFormValues>[1];
  19. export interface SerializeBodyParams {
  20. form: HTMLFormElement,
  21. encType: string,
  22. serializers?: EncTypeSerializerMap,
  23. serializerOptions?: SerializerOptions,
  24. methodFormKey?: string,
  25. preventRedirectFormKey?: string,
  26. }
  27. export const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = {
  28. [ENCTYPE_APPLICATION_JSON]: (data: unknown) => JSON.stringify(data),
  29. };
  30. export const serializeBody = (params: SerializeBodyParams) => {
  31. const {
  32. form,
  33. encType,
  34. serializers = DEFAULT_ENCTYPE_SERIALIZERS,
  35. serializerOptions,
  36. methodFormKey = METHOD_FORM_KEY,
  37. preventRedirectFormKey = PREVENT_REDIRECT_FORM_KEY,
  38. } = params;
  39. if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) {
  40. const searchParams = new URLSearchParams(form);
  41. searchParams.delete(methodFormKey);
  42. searchParams.delete(preventRedirectFormKey);
  43. return searchParams;
  44. }
  45. if (encType === ENCTYPE_MULTIPART_FORM_DATA) {
  46. // type error when provided a submitter element for some reason...
  47. const FormDataUnknown = FormData as unknown as {
  48. new(formElement?: HTMLElement, submitter?: HTMLElement): FormData & {
  49. entries: () => IterableIterator<[string, unknown]>;
  50. };
  51. };
  52. const formData = new FormDataUnknown(form, serializerOptions?.submitter);
  53. const emptyFiles = Array.from(formData.entries())
  54. .filter(([, value]) => (
  55. value instanceof File && value.size === 0 && value.name === ''
  56. ));
  57. emptyFiles.forEach(([key]) => formData.delete(key));
  58. formData.delete(methodFormKey);
  59. formData.delete(preventRedirectFormKey);
  60. return formData;
  61. }
  62. if (typeof serializers[encType] === 'function') {
  63. const {
  64. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  65. [methodFormKey]: _method,
  66. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  67. [preventRedirectFormKey]: _preventRedirect,
  68. ...formValues
  69. } = getFormValues(form, serializerOptions);
  70. return serializers[encType](formValues);
  71. }
  72. throw new Error(`Unsupported encType: ${encType}`);
  73. };
  74. export type EncTypeDeserializer = (data: string) => unknown;
  75. export type EncTypeDeserializerMap = Record<string, EncTypeDeserializer>;
  76. export interface DeserializeBodyParams {
  77. req: IncomingMessage,
  78. deserializers?: EncTypeDeserializerMap,
  79. }
  80. export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = {
  81. [ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>,
  82. [ENCTYPE_X_WWW_FORM_URLENCODED]: (data: string) => Object.fromEntries(
  83. new URLSearchParams(data).entries(),
  84. ),
  85. };
  86. export const deserializeFormObjectBody = async (params: DeserializeBodyParams) => {
  87. const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params;
  88. const contentType = req.headers[CONTENT_TYPE_HEADER_KEY] ?? ENCTYPE_APPLICATION_OCTET_STREAM;
  89. if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) {
  90. return parseMultipartFormData(req);
  91. }
  92. const bodyRaw = await getBody(req);
  93. const encoding = (
  94. req.headers[CONTENT_ENCODING_HEADER_KEY] ?? DEFAULT_ENCODING
  95. ) as BufferEncoding;
  96. const { [contentType]: theDeserializer } = deserializers;
  97. if (typeof theDeserializer !== 'function') {
  98. throw new Error(`Could not deserialize body with content type: ${contentType}`);
  99. }
  100. return theDeserializer(bodyRaw.toString(encoding)) as Record<string, unknown>;
  101. };