forked from clerk/javascript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrequest.ts
More file actions
213 lines (188 loc) · 6.2 KB
/
Copy pathrequest.ts
File metadata and controls
213 lines (188 loc) · 6.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import { ClerkAPIResponseError, parseError } from '@clerk/shared/error';
import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/types';
import snakecaseKeys from 'snakecase-keys';
import { API_URL, API_VERSION, constants, SUPPORTED_BAPI_VERSION, USER_AGENT } from '../constants';
import { runtime } from '../runtime';
import { assertValidSecretKey } from '../util/optionsAssertions';
import { joinPaths } from '../util/path';
import { deserialize } from './resources/Deserializer';
export type ClerkBackendApiRequestOptions = {
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
queryParams?: Record<string, unknown>;
headerParams?: Record<string, string>;
bodyParams?: object;
formData?: FormData;
} & (
| {
url: string;
path?: string;
}
| {
url?: string;
path: string;
}
);
export type ClerkBackendApiResponse<T> =
| {
data: T;
errors: null;
totalCount?: number;
}
| {
data: null;
errors: ClerkAPIError[];
totalCount?: never;
clerkTraceId?: string;
status?: number;
statusText?: string;
};
export type RequestFunction = ReturnType<typeof buildRequest>;
type BuildRequestOptions = {
/* Secret Key */
secretKey?: string;
/* Backend API URL */
apiUrl?: string;
/* Backend API version */
apiVersion?: string;
/* Library/SDK name */
userAgent?: string;
/**
* Allow requests without specifying a secret key. In most cases this should be set to `false`.
* Defaults to `true`.
*/
requireSecretKey?: boolean;
};
export function buildRequest(options: BuildRequestOptions) {
const requestFn = async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
const {
secretKey,
requireSecretKey = true,
apiUrl = API_URL,
apiVersion = API_VERSION,
userAgent = USER_AGENT,
} = options;
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;
if (requireSecretKey) {
assertValidSecretKey(secretKey);
}
const url = joinPaths(apiUrl, apiVersion, path);
// Build final URL with search parameters
const finalUrl = new URL(url);
if (queryParams) {
// Snakecase query parameters
const snakecasedQueryParams = snakecaseKeys({ ...queryParams });
// Support array values for queryParams such as { foo: [42, 43] }
for (const [key, val] of Object.entries(snakecasedQueryParams)) {
if (val) {
[val].flat().forEach(v => finalUrl.searchParams.append(key, v as string));
}
}
}
// Build headers
const headers: Record<string, any> = {
Authorization: `Bearer ${secretKey}`,
'Clerk-API-Version': SUPPORTED_BAPI_VERSION,
'User-Agent': userAgent,
...headerParams,
};
let res: Response | undefined;
try {
if (formData) {
res = await runtime.fetch(finalUrl.href, {
method,
headers,
body: formData,
});
} else {
// Enforce application/json for all non form-data requests
headers['Content-Type'] = 'application/json';
// Build body
const hasBody = method !== 'GET' && bodyParams && Object.keys(bodyParams).length > 0;
const body = hasBody ? { body: JSON.stringify(snakecaseKeys(bodyParams, { deep: false })) } : null;
res = await runtime.fetch(finalUrl.href, {
method,
headers,
...body,
});
}
// TODO: Parse JSON or Text response based on a response header
const isJSONResponse =
res?.headers && res.headers?.get(constants.Headers.ContentType) === constants.ContentTypes.Json;
const responseBody = await (isJSONResponse ? res.json() : res.text());
if (!res.ok) {
return {
data: null,
errors: parseErrors(responseBody),
status: res?.status,
statusText: res?.statusText,
clerkTraceId: getTraceId(responseBody, res?.headers),
};
}
return {
...deserialize<T>(responseBody),
errors: null,
};
} catch (err) {
if (err instanceof Error) {
return {
data: null,
errors: [
{
code: 'unexpected_error',
message: err.message || 'Unexpected error',
},
],
clerkTraceId: getTraceId(err, res?.headers),
};
}
return {
data: null,
errors: parseErrors(err),
status: res?.status,
statusText: res?.statusText,
clerkTraceId: getTraceId(err, res?.headers),
};
}
};
return withLegacyRequestReturn(requestFn);
}
// Returns either clerk_trace_id if present in response json, otherwise defaults to CF-Ray header
// If the request failed before receiving a response, returns undefined
function getTraceId(data: unknown, headers?: Headers): string {
if (data && typeof data === 'object' && 'clerk_trace_id' in data && typeof data.clerk_trace_id === 'string') {
return data.clerk_trace_id;
}
const cfRay = headers?.get('cf-ray');
return cfRay || '';
}
function parseErrors(data: unknown): ClerkAPIError[] {
if (!!data && typeof data === 'object' && 'errors' in data) {
const errors = data.errors as ClerkAPIErrorJSON[];
return errors.length > 0 ? errors.map(parseError) : [];
}
return [];
}
type LegacyRequestFunction = <T>(requestOptions: ClerkBackendApiRequestOptions) => Promise<T>;
// TODO(dimkl): Will be probably be dropped in next major version
function withLegacyRequestReturn(cb: any): LegacyRequestFunction {
return async (...args) => {
// @ts-ignore
const { data, errors, totalCount, status, statusText, clerkTraceId } = await cb<T>(...args);
if (errors) {
// instead of passing `data: errors`, we have set the `error.errors` because
// the errors returned from callback is already parsed and passing them as `data`
// will not be able to assign them to the instance
const error = new ClerkAPIResponseError(statusText || '', {
data: [],
status,
clerkTraceId,
});
error.errors = errors;
throw error;
}
if (typeof totalCount !== 'undefined') {
return { data, totalCount };
}
return data;
};
}