axios.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. // biome-ignore lint/style/noRestrictedImports: This is the axios wrapper utility
  2. import axios from 'axios';
  3. import { formatISO } from 'date-fns';
  4. import qs from 'qs';
  5. // biome-ignore lint/style/noRestrictedImports: Re-exporting axios types
  6. export * from 'axios';
  7. const isoDateRegex =
  8. /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(Z|[+-]\d{2}:\d{2})$/;
  9. export type DateConvertible =
  10. | string
  11. | number
  12. | boolean
  13. | Date
  14. | null
  15. | undefined
  16. | DateConvertible[]
  17. | { [key: string]: DateConvertible };
  18. /**
  19. * Converts string to dates recursively.
  20. *
  21. * @param data - Data to be transformed to Date if applicable.
  22. * @param seen - Set containing data that has been through the function before.
  23. * @returns - Data containing transformed Dates.
  24. */
  25. function convertStringsToDatesRecursive(
  26. data: DateConvertible,
  27. seen: Set<unknown>,
  28. ): DateConvertible {
  29. if (typeof data !== 'object' || data === null) {
  30. if (typeof data === 'string' && isoDateRegex.test(data)) {
  31. return new Date(data);
  32. }
  33. return data;
  34. }
  35. // Check for circular reference
  36. if (seen.has(data)) {
  37. return data;
  38. }
  39. seen.add(data);
  40. if (Array.isArray(data)) {
  41. return data.map((item) => convertStringsToDatesRecursive(item, seen));
  42. }
  43. const newData: Record<string, DateConvertible> = {};
  44. for (const key of Object.keys(data as object)) {
  45. const value = (data as Record<string, DateConvertible>)[key];
  46. if (typeof value === 'string' && isoDateRegex.test(value)) {
  47. newData[key] = new Date(value);
  48. } else if (typeof value === 'object' && value !== null) {
  49. newData[key] = convertStringsToDatesRecursive(value, seen);
  50. } else {
  51. newData[key] = value;
  52. }
  53. }
  54. return newData;
  55. }
  56. // Function overloads for better type inference
  57. export function convertStringsToDates(data: string): string | Date;
  58. export function convertStringsToDates<T extends DateConvertible>(
  59. data: T,
  60. ): DateConvertible;
  61. export function convertStringsToDates<T extends DateConvertible[]>(
  62. data: T,
  63. ): DateConvertible[];
  64. export function convertStringsToDates<
  65. T extends Record<string, DateConvertible>,
  66. >(data: T): Record<string, DateConvertible>;
  67. export function convertStringsToDates(data: DateConvertible): DateConvertible {
  68. return convertStringsToDatesRecursive(data, new Set());
  69. }
  70. // Determine the base array of transformers
  71. let baseTransformers = axios.defaults.transformResponse;
  72. if (baseTransformers == null) {
  73. baseTransformers = [];
  74. } else if (!Array.isArray(baseTransformers)) {
  75. // If it's a single transformer function, wrap it in an array
  76. baseTransformers = [baseTransformers];
  77. }
  78. const customAxios = axios.create({
  79. headers: {
  80. 'X-Requested-With': 'XMLHttpRequest',
  81. 'Content-Type': 'application/json',
  82. },
  83. transformResponse: baseTransformers.concat((data) => {
  84. return convertStringsToDates(data);
  85. }),
  86. });
  87. // serialize Date config: https://github.com/axios/axios/issues/1548#issuecomment-548306666
  88. customAxios.interceptors.request.use((config) => {
  89. config.paramsSerializer = (params) =>
  90. qs.stringify(params, {
  91. serializeDate: (date: Date) => {
  92. return formatISO(date, { representation: 'complete' });
  93. },
  94. });
  95. return config;
  96. });
  97. export default customAxios;