diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/server-commands/src/requests/requests.ts | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/server-commands/src/requests/requests.ts')
-rw-r--r-- | packages/server-commands/src/requests/requests.ts | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/packages/server-commands/src/requests/requests.ts b/packages/server-commands/src/requests/requests.ts new file mode 100644 index 000000000..ac143ea5d --- /dev/null +++ b/packages/server-commands/src/requests/requests.ts | |||
@@ -0,0 +1,260 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-floating-promises */ | ||
2 | |||
3 | import { decode } from 'querystring' | ||
4 | import request from 'supertest' | ||
5 | import { URL } from 'url' | ||
6 | import { pick } from '@peertube/peertube-core-utils' | ||
7 | import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models' | ||
8 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
9 | |||
10 | export type CommonRequestParams = { | ||
11 | url: string | ||
12 | path?: string | ||
13 | contentType?: string | ||
14 | responseType?: string | ||
15 | range?: string | ||
16 | redirects?: number | ||
17 | accept?: string | ||
18 | host?: string | ||
19 | token?: string | ||
20 | headers?: { [ name: string ]: string } | ||
21 | type?: string | ||
22 | xForwardedFor?: string | ||
23 | expectedStatus?: HttpStatusCodeType | ||
24 | } | ||
25 | |||
26 | function makeRawRequest (options: { | ||
27 | url: string | ||
28 | token?: string | ||
29 | expectedStatus?: HttpStatusCodeType | ||
30 | range?: string | ||
31 | query?: { [ id: string ]: string } | ||
32 | method?: 'GET' | 'POST' | ||
33 | headers?: { [ name: string ]: string } | ||
34 | }) { | ||
35 | const { host, protocol, pathname } = new URL(options.url) | ||
36 | |||
37 | const reqOptions = { | ||
38 | url: `${protocol}//${host}`, | ||
39 | path: pathname, | ||
40 | contentType: undefined, | ||
41 | |||
42 | ...pick(options, [ 'expectedStatus', 'range', 'token', 'query', 'headers' ]) | ||
43 | } | ||
44 | |||
45 | if (options.method === 'POST') { | ||
46 | return makePostBodyRequest(reqOptions) | ||
47 | } | ||
48 | |||
49 | return makeGetRequest(reqOptions) | ||
50 | } | ||
51 | |||
52 | function makeGetRequest (options: CommonRequestParams & { | ||
53 | query?: any | ||
54 | rawQuery?: string | ||
55 | }) { | ||
56 | const req = request(options.url).get(options.path) | ||
57 | |||
58 | if (options.query) req.query(options.query) | ||
59 | if (options.rawQuery) req.query(options.rawQuery) | ||
60 | |||
61 | return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) | ||
62 | } | ||
63 | |||
64 | function makeHTMLRequest (url: string, path: string) { | ||
65 | return makeGetRequest({ | ||
66 | url, | ||
67 | path, | ||
68 | accept: 'text/html', | ||
69 | expectedStatus: HttpStatusCode.OK_200 | ||
70 | }) | ||
71 | } | ||
72 | |||
73 | function makeActivityPubGetRequest (url: string, path: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { | ||
74 | return makeGetRequest({ | ||
75 | url, | ||
76 | path, | ||
77 | expectedStatus, | ||
78 | accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8' | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | function makeDeleteRequest (options: CommonRequestParams & { | ||
83 | query?: any | ||
84 | rawQuery?: string | ||
85 | }) { | ||
86 | const req = request(options.url).delete(options.path) | ||
87 | |||
88 | if (options.query) req.query(options.query) | ||
89 | if (options.rawQuery) req.query(options.rawQuery) | ||
90 | |||
91 | return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) | ||
92 | } | ||
93 | |||
94 | function makeUploadRequest (options: CommonRequestParams & { | ||
95 | method?: 'POST' | 'PUT' | ||
96 | |||
97 | fields: { [ fieldName: string ]: any } | ||
98 | attaches?: { [ attachName: string ]: any | any[] } | ||
99 | }) { | ||
100 | let req = options.method === 'PUT' | ||
101 | ? request(options.url).put(options.path) | ||
102 | : request(options.url).post(options.path) | ||
103 | |||
104 | req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) | ||
105 | |||
106 | buildFields(req, options.fields) | ||
107 | |||
108 | Object.keys(options.attaches || {}).forEach(attach => { | ||
109 | const value = options.attaches[attach] | ||
110 | if (!value) return | ||
111 | |||
112 | if (Array.isArray(value)) { | ||
113 | req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) | ||
114 | } else { | ||
115 | req.attach(attach, buildAbsoluteFixturePath(value)) | ||
116 | } | ||
117 | }) | ||
118 | |||
119 | return req | ||
120 | } | ||
121 | |||
122 | function makePostBodyRequest (options: CommonRequestParams & { | ||
123 | fields?: { [ fieldName: string ]: any } | ||
124 | }) { | ||
125 | const req = request(options.url).post(options.path) | ||
126 | .send(options.fields) | ||
127 | |||
128 | return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) | ||
129 | } | ||
130 | |||
131 | function makePutBodyRequest (options: { | ||
132 | url: string | ||
133 | path: string | ||
134 | token?: string | ||
135 | fields: { [ fieldName: string ]: any } | ||
136 | expectedStatus?: HttpStatusCodeType | ||
137 | headers?: { [name: string]: string } | ||
138 | }) { | ||
139 | const req = request(options.url).put(options.path) | ||
140 | .send(options.fields) | ||
141 | |||
142 | return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) | ||
143 | } | ||
144 | |||
145 | function decodeQueryString (path: string) { | ||
146 | return decode(path.split('?')[1]) | ||
147 | } | ||
148 | |||
149 | // --------------------------------------------------------------------------- | ||
150 | |||
151 | function unwrapBody <T> (test: request.Test): Promise<T> { | ||
152 | return test.then(res => res.body) | ||
153 | } | ||
154 | |||
155 | function unwrapText (test: request.Test): Promise<string> { | ||
156 | return test.then(res => res.text) | ||
157 | } | ||
158 | |||
159 | function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> { | ||
160 | return test.then(res => { | ||
161 | if (res.body instanceof Buffer) { | ||
162 | try { | ||
163 | return JSON.parse(new TextDecoder().decode(res.body)) | ||
164 | } catch (err) { | ||
165 | console.error('Cannot decode JSON.', { res, body: res.body instanceof Buffer ? res.body.toString() : res.body }) | ||
166 | throw err | ||
167 | } | ||
168 | } | ||
169 | |||
170 | if (res.text) { | ||
171 | try { | ||
172 | return JSON.parse(res.text) | ||
173 | } catch (err) { | ||
174 | console.error('Cannot decode json', { res, text: res.text }) | ||
175 | throw err | ||
176 | } | ||
177 | } | ||
178 | |||
179 | return res.body | ||
180 | }) | ||
181 | } | ||
182 | |||
183 | function unwrapTextOrDecode (test: request.Test): Promise<string> { | ||
184 | return test.then(res => res.text || new TextDecoder().decode(res.body)) | ||
185 | } | ||
186 | |||
187 | // --------------------------------------------------------------------------- | ||
188 | |||
189 | export { | ||
190 | makeHTMLRequest, | ||
191 | makeGetRequest, | ||
192 | decodeQueryString, | ||
193 | makeUploadRequest, | ||
194 | makePostBodyRequest, | ||
195 | makePutBodyRequest, | ||
196 | makeDeleteRequest, | ||
197 | makeRawRequest, | ||
198 | makeActivityPubGetRequest, | ||
199 | unwrapBody, | ||
200 | unwrapTextOrDecode, | ||
201 | unwrapBodyOrDecodeToJSON, | ||
202 | unwrapText | ||
203 | } | ||
204 | |||
205 | // --------------------------------------------------------------------------- | ||
206 | |||
207 | function buildRequest (req: request.Test, options: CommonRequestParams) { | ||
208 | if (options.contentType) req.set('Accept', options.contentType) | ||
209 | if (options.responseType) req.responseType(options.responseType) | ||
210 | if (options.token) req.set('Authorization', 'Bearer ' + options.token) | ||
211 | if (options.range) req.set('Range', options.range) | ||
212 | if (options.accept) req.set('Accept', options.accept) | ||
213 | if (options.host) req.set('Host', options.host) | ||
214 | if (options.redirects) req.redirects(options.redirects) | ||
215 | if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor) | ||
216 | if (options.type) req.type(options.type) | ||
217 | |||
218 | Object.keys(options.headers || {}).forEach(name => { | ||
219 | req.set(name, options.headers[name]) | ||
220 | }) | ||
221 | |||
222 | return req.expect(res => { | ||
223 | if (options.expectedStatus && res.status !== options.expectedStatus) { | ||
224 | const err = new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + | ||
225 | `\nThe server responded: "${res.body?.error ?? res.text}".\n` + | ||
226 | 'You may take a closer look at the logs. To see how to do so, check out this page: ' + | ||
227 | 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs'); | ||
228 | |||
229 | (err as any).res = res | ||
230 | |||
231 | throw err | ||
232 | } | ||
233 | |||
234 | return res | ||
235 | }) | ||
236 | } | ||
237 | |||
238 | function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) { | ||
239 | if (!fields) return | ||
240 | |||
241 | let formKey: string | ||
242 | |||
243 | for (const key of Object.keys(fields)) { | ||
244 | if (namespace) formKey = `${namespace}[${key}]` | ||
245 | else formKey = key | ||
246 | |||
247 | if (fields[key] === undefined) continue | ||
248 | |||
249 | if (Array.isArray(fields[key]) && fields[key].length === 0) { | ||
250 | req.field(key, []) | ||
251 | continue | ||
252 | } | ||
253 | |||
254 | if (fields[key] !== null && typeof fields[key] === 'object') { | ||
255 | buildFields(req, fields[key], formKey) | ||
256 | } else { | ||
257 | req.field(formKey, fields[key]) | ||
258 | } | ||
259 | } | ||
260 | } | ||