apiHelpers.ts132 lines · main
1import type { IncomingHttpHeaders } from 'node:http'
2import { snakeCase } from 'lodash'
3import z from 'zod'
4
5import { IS_PLATFORM } from '@/lib/constants'
6
7/**
8 * Construct headers for api request.
9 * For platform, it will include apiKey into the provided headers.
10 *
11 * To prevent relay frontend request headers like useragent, referrer... into the middleware requests.
12 * We will only keep the header keys that are in this list: Accept, Authorization, Content-Type, x-connection-encrypted
13 */
14export function constructHeaders(headers: { [prop: string]: any }) {
15 if (headers) {
16 const cleansedHeaders = {
17 Accept: headers.Accept,
18 Authorization: headers.Authorization,
19 cookie: headers.cookie,
20 'Content-Type': headers['Content-Type'],
21 'x-connection-encrypted': headers['x-connection-encrypted'],
22 } as any
23 // clean up key with underfined value
24 Object.keys(cleansedHeaders).forEach((key) =>
25 cleansedHeaders[key] === undefined ? delete cleansedHeaders[key] : {}
26 )
27 return {
28 ...cleansedHeaders,
29 // [Joshen] JFYI both Alaister and I checked on this and realised this might not be used actually
30 // Could be safe to remove but leaving it here for now
31 ...(!IS_PLATFORM && { apiKey: `${process.env.BRIVEN_SERVICE_KEY}` }),
32 }
33 } else {
34 return {
35 'Content-Type': 'application/json',
36 Accept: 'application/json',
37 }
38 }
39}
40
41// Typically for HTTP payloads
42// @ts-ignore
43export const toSnakeCase = (object) => {
44 const snakeCaseObject = {}
45 const snakeCaseArray = []
46
47 if (!object) return null
48
49 if (Array.isArray(object)) {
50 for (const item of object) {
51 if (typeof item === 'object') {
52 snakeCaseArray.push(toSnakeCase(item))
53 } else {
54 snakeCaseArray.push(item)
55 }
56 }
57 return snakeCaseArray
58 } else if (typeof object === 'object') {
59 for (const key of Object.keys(object)) {
60 if (typeof object[key] === 'object') {
61 // @ts-ignore
62 snakeCaseObject[snakeCase(key)] = toSnakeCase(object[key])
63 } else {
64 // @ts-ignore
65 snakeCaseObject[snakeCase(key)] = object[key]
66 }
67 }
68 return snakeCaseObject
69 } else {
70 return object
71 }
72}
73
74/**
75 * Converts Node.js `IncomingHttpHeaders` to Fetch API `Headers`.
76 */
77export function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
78 const headers = new Headers()
79 for (const [key, value] of Object.entries(nodeHeaders)) {
80 if (Array.isArray(value)) {
81 value.forEach((v) => headers.append(key, v))
82 } else if (value !== undefined) {
83 headers.append(key, value)
84 }
85 }
86 return headers
87}
88
89/**
90 * Zod transformer to parse boolean values from strings.
91 *
92 * Use when accepting a boolean value in a query parameter.
93 */
94export function zBooleanString(errorMsg?: string) {
95 return z.string().transform((value, ctx) => {
96 if (value === 'true') {
97 return true
98 }
99
100 if (value === 'false') {
101 return false
102 }
103
104 ctx.addIssue({
105 code: z.ZodIssueCode.custom,
106 message: errorMsg || 'must be a boolean string',
107 })
108 return z.NEVER
109 })
110}
111
112/**
113 * Transform a comma-separated string into an array of strings.
114 *
115 * Use when accepting a list of values in a query parameter.
116 */
117export function commaSeparatedStringIntoArray(value: string): string[] {
118 return value
119 .split(',')
120 .map((v) => v.trim())
121 .filter(Boolean)
122}
123
124export class InternalServerError extends Error {
125 constructor(
126 message: string,
127 public details?: Record<string, unknown>
128 ) {
129 super(message)
130 this.name = 'InternalServerError'
131 }
132}