core-services-sdk 1.3.86 → 1.3.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/http/http.js +162 -13
- package/src/postgresql/start-stop-postgres-docker.js +10 -0
- package/src/rabbit-mq/start-stop-rabbitmq.js +13 -0
- package/tests/http/http.int.test.js +310 -0
- package/tests/http/http.unit.test.js +101 -2
- package/tests/rabbit-mq/rabbit-mq.test.js +1 -1
- package/tests/repositories/BaseRepository.int.test.js +2 -2
- package/tests/repositories/TenantScopedRepository.int.test.js +2 -2
- package/types/http/http.d.ts +4 -3
- package/vitest.config.js +1 -1
package/package.json
CHANGED
package/src/http/http.js
CHANGED
|
@@ -183,6 +183,135 @@ const getResponsePayload = async (response, responseType) => {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Checks if value is a Node.js Readable stream.
|
|
188
|
+
*
|
|
189
|
+
* @param {any} value
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
const isReadableStream = (value) => {
|
|
193
|
+
return value && typeof value === 'object' && typeof value.pipe === 'function'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Checks if value is a TypedArray (Uint8Array, etc).
|
|
198
|
+
*
|
|
199
|
+
* @param {any} value
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
const isTypedArray = (value) => {
|
|
203
|
+
return ArrayBuffer.isView(value)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Normalizes the request body before sending it via fetch.
|
|
208
|
+
*
|
|
209
|
+
* Ensures backward compatibility while allowing additional body types.
|
|
210
|
+
* - undefined/null → no body sent
|
|
211
|
+
* - string → sent as-is
|
|
212
|
+
* - URLSearchParams → sent as-is (fetch serializes automatically)
|
|
213
|
+
* - FormData → sent as-is (fetch sets multipart boundary automatically)
|
|
214
|
+
* - ArrayBuffer / Blob → sent as-is
|
|
215
|
+
* - Buffer → sent as-is
|
|
216
|
+
* - TypedArray → sent as-is
|
|
217
|
+
* - Readable stream → sent as-is
|
|
218
|
+
* - object / array → JSON.stringify applied and Content-Type ensured
|
|
219
|
+
*
|
|
220
|
+
* @param {any} body
|
|
221
|
+
* @param {Record<string,string>} headers
|
|
222
|
+
* @returns {{ body:any, headers:Record<string,string> }}
|
|
223
|
+
*/
|
|
224
|
+
const prepareRequestBody = (body, headers = {}) => {
|
|
225
|
+
if (body === undefined || body === null) {
|
|
226
|
+
return { body: undefined, headers }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (typeof body === 'string') {
|
|
230
|
+
return { body, headers }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (body instanceof URLSearchParams) {
|
|
234
|
+
return { body, headers }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (body instanceof FormData) {
|
|
238
|
+
return { body, headers }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (Buffer.isBuffer(body)) {
|
|
242
|
+
return { body, headers }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (body instanceof ArrayBuffer) {
|
|
246
|
+
return { body, headers }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (isTypedArray(body)) {
|
|
250
|
+
return { body, headers }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
254
|
+
return { body, headers }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isReadableStream(body)) {
|
|
258
|
+
return { body, headers }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof body === 'object') {
|
|
262
|
+
return {
|
|
263
|
+
body: JSON.stringify(body),
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
...headers,
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { body, headers }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resolves headers safely based on body type.
|
|
276
|
+
*
|
|
277
|
+
* Adds default headers only when no Content-Type is provided
|
|
278
|
+
* AND the body is a plain JSON object or array.
|
|
279
|
+
*
|
|
280
|
+
* @param {Record<string,string>} preparedHeaders
|
|
281
|
+
* @param {any} body
|
|
282
|
+
* @param {Record<string,string>} defaultHeaders
|
|
283
|
+
* @returns {Record<string,string>}
|
|
284
|
+
*/
|
|
285
|
+
const resolveHeaders = (
|
|
286
|
+
preparedHeaders = {},
|
|
287
|
+
body,
|
|
288
|
+
defaultHeaders = JSON_HEADER,
|
|
289
|
+
) => {
|
|
290
|
+
const hasContentType = Object.keys(preparedHeaders).some(
|
|
291
|
+
(key) => key.toLowerCase() === 'content-type',
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (hasContentType) {
|
|
295
|
+
return preparedHeaders
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const isPlainObject =
|
|
299
|
+
body !== null && typeof body === 'object' && body.constructor === Object
|
|
300
|
+
|
|
301
|
+
const isArray = Array.isArray(body)
|
|
302
|
+
|
|
303
|
+
const isJsonCandidate = isPlainObject || isArray
|
|
304
|
+
|
|
305
|
+
if (!isJsonCandidate) {
|
|
306
|
+
return preparedHeaders
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...defaultHeaders,
|
|
311
|
+
...preparedHeaders,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
186
315
|
/**
|
|
187
316
|
* Sends an HTTP GET request.
|
|
188
317
|
*
|
|
@@ -199,7 +328,10 @@ export const get = async ({
|
|
|
199
328
|
const response = await fetch(url, {
|
|
200
329
|
...extraParams,
|
|
201
330
|
method: HTTP_METHODS.GET,
|
|
202
|
-
headers: {
|
|
331
|
+
headers: {
|
|
332
|
+
Accept: 'application/json',
|
|
333
|
+
...headers,
|
|
334
|
+
},
|
|
203
335
|
})
|
|
204
336
|
await checkStatus(response)
|
|
205
337
|
return getResponsePayload(response, expectedType)
|
|
@@ -219,11 +351,15 @@ export const post = async ({
|
|
|
219
351
|
extraParams = {},
|
|
220
352
|
expectedType = ResponseType.json,
|
|
221
353
|
}) => {
|
|
354
|
+
const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
|
|
355
|
+
body,
|
|
356
|
+
headers,
|
|
357
|
+
)
|
|
222
358
|
const response = await fetch(url, {
|
|
223
359
|
...extraParams,
|
|
224
360
|
method: HTTP_METHODS.POST,
|
|
225
|
-
headers:
|
|
226
|
-
body:
|
|
361
|
+
headers: resolveHeaders(preparedHeaders, body),
|
|
362
|
+
body: preparedBody,
|
|
227
363
|
})
|
|
228
364
|
await checkStatus(response)
|
|
229
365
|
return getResponsePayload(response, expectedType)
|
|
@@ -243,11 +379,15 @@ export const put = async ({
|
|
|
243
379
|
extraParams = {},
|
|
244
380
|
expectedType = ResponseType.json,
|
|
245
381
|
}) => {
|
|
382
|
+
const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
|
|
383
|
+
body,
|
|
384
|
+
headers,
|
|
385
|
+
)
|
|
246
386
|
const response = await fetch(url, {
|
|
247
387
|
...extraParams,
|
|
248
388
|
method: HTTP_METHODS.PUT,
|
|
249
|
-
headers:
|
|
250
|
-
body:
|
|
389
|
+
headers: resolveHeaders(preparedHeaders, body),
|
|
390
|
+
body: preparedBody,
|
|
251
391
|
})
|
|
252
392
|
await checkStatus(response)
|
|
253
393
|
return getResponsePayload(response, expectedType)
|
|
@@ -267,11 +407,15 @@ export const patch = async ({
|
|
|
267
407
|
extraParams = {},
|
|
268
408
|
expectedType = ResponseType.json,
|
|
269
409
|
}) => {
|
|
410
|
+
const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
|
|
411
|
+
body,
|
|
412
|
+
headers,
|
|
413
|
+
)
|
|
270
414
|
const response = await fetch(url, {
|
|
271
415
|
...extraParams,
|
|
272
416
|
method: HTTP_METHODS.PATCH,
|
|
273
|
-
headers:
|
|
274
|
-
body:
|
|
417
|
+
headers: resolveHeaders(preparedHeaders, body),
|
|
418
|
+
body: preparedBody,
|
|
275
419
|
})
|
|
276
420
|
await checkStatus(response)
|
|
277
421
|
return getResponsePayload(response, expectedType)
|
|
@@ -291,11 +435,15 @@ export const deleteApi = async ({
|
|
|
291
435
|
extraParams = {},
|
|
292
436
|
expectedType = ResponseType.json,
|
|
293
437
|
}) => {
|
|
438
|
+
const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
|
|
439
|
+
body,
|
|
440
|
+
headers,
|
|
441
|
+
)
|
|
294
442
|
const response = await fetch(url, {
|
|
295
443
|
...extraParams,
|
|
296
444
|
method: HTTP_METHODS.DELETE,
|
|
297
|
-
headers:
|
|
298
|
-
...(
|
|
445
|
+
headers: resolveHeaders(preparedHeaders, body),
|
|
446
|
+
...(preparedBody ? { body: preparedBody } : {}),
|
|
299
447
|
})
|
|
300
448
|
await checkStatus(response)
|
|
301
449
|
return getResponsePayload(response, expectedType)
|
|
@@ -312,7 +460,7 @@ export const head = async ({ url, headers = {}, extraParams = {} }) => {
|
|
|
312
460
|
const response = await fetch(url, {
|
|
313
461
|
...extraParams,
|
|
314
462
|
method: HTTP_METHODS.HEAD,
|
|
315
|
-
headers
|
|
463
|
+
headers,
|
|
316
464
|
})
|
|
317
465
|
|
|
318
466
|
await checkStatus(response)
|
|
@@ -328,8 +476,9 @@ export const head = async ({ url, headers = {}, extraParams = {} }) => {
|
|
|
328
476
|
* put: (options: HttpPutOptions) => Promise<any>,
|
|
329
477
|
* post: (options: HttpPostOptions) => Promise<any>,
|
|
330
478
|
* patch: (options: HttpPatchOptions) => Promise<any>,
|
|
331
|
-
*
|
|
332
|
-
*
|
|
479
|
+
* head: (options: HttpHeadOptions) => Promise<Response>,
|
|
480
|
+
* deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
481
|
+
*
|
|
333
482
|
* }}
|
|
334
483
|
*/
|
|
335
484
|
|
|
@@ -337,7 +486,7 @@ export const http = {
|
|
|
337
486
|
get,
|
|
338
487
|
put,
|
|
339
488
|
post,
|
|
489
|
+
head,
|
|
340
490
|
patch,
|
|
341
491
|
deleteApi,
|
|
342
|
-
head,
|
|
343
492
|
}
|
|
@@ -39,6 +39,16 @@ export function startPostgres({
|
|
|
39
39
|
|
|
40
40
|
stopPostgres(containerName)
|
|
41
41
|
|
|
42
|
+
// Kill any container that might still be holding the port
|
|
43
|
+
try {
|
|
44
|
+
const containerId = execSync(`docker ps -q --filter "publish=${port}"`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
}).trim()
|
|
47
|
+
if (containerId) {
|
|
48
|
+
execSync(`docker rm -f ${containerId}`, { stdio: 'ignore' })
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
|
|
42
52
|
execSync(
|
|
43
53
|
`docker run -d \
|
|
44
54
|
--name ${containerName} \
|
|
@@ -35,6 +35,18 @@ export function startRabbit({ containerName, ...rest }) {
|
|
|
35
35
|
execSync(`docker rm -f ${containerName}`, { stdio: 'ignore' })
|
|
36
36
|
} catch {}
|
|
37
37
|
|
|
38
|
+
// Kill any containers that might still be holding the ports
|
|
39
|
+
for (const port of [rest.amqpPort, rest.uiPort]) {
|
|
40
|
+
try {
|
|
41
|
+
const id = execSync(`docker ps -q --filter "publish=${port}"`, {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
}).trim()
|
|
44
|
+
if (id) {
|
|
45
|
+
execSync(`docker rm -f ${id}`, { stdio: 'ignore' })
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
execSync(
|
|
39
51
|
`docker run -d \
|
|
40
52
|
--name ${containerName} \
|
|
@@ -42,6 +54,7 @@ export function startRabbit({ containerName, ...rest }) {
|
|
|
42
54
|
-e RABBITMQ_DEFAULT_PASS=${rest.pass} \
|
|
43
55
|
-p ${rest.amqpPort}:5672 \
|
|
44
56
|
-p ${rest.uiPort}:15672 \
|
|
57
|
+
--tmpfs /var/lib/rabbitmq \
|
|
45
58
|
--health-cmd="rabbitmq-diagnostics -q ping" \
|
|
46
59
|
--health-interval=5s \
|
|
47
60
|
--health-timeout=5s \
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
+
import httpServer from 'node:http'
|
|
4
|
+
import { http } from '../../src/http/http.js'
|
|
5
|
+
|
|
6
|
+
let server
|
|
7
|
+
let baseUrl
|
|
8
|
+
|
|
9
|
+
function collectRaw(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const chunks = []
|
|
12
|
+
|
|
13
|
+
req.on('data', (chunk) => {
|
|
14
|
+
chunks.push(chunk)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
const buffer = Buffer.concat(chunks)
|
|
19
|
+
resolve(buffer)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
server = httpServer.createServer(async (req, res) => {
|
|
26
|
+
const raw = await collectRaw(req)
|
|
27
|
+
|
|
28
|
+
res.setHeader('content-type', 'application/json')
|
|
29
|
+
|
|
30
|
+
res.end(
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
method: req.method,
|
|
33
|
+
headers: req.headers,
|
|
34
|
+
body: raw.toString(),
|
|
35
|
+
bodyLength: raw.length,
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await new Promise((resolve) => {
|
|
41
|
+
server.listen(0, () => {
|
|
42
|
+
const { port } = server.address()
|
|
43
|
+
baseUrl = `http://localhost:${port}`
|
|
44
|
+
resolve()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await new Promise((resolve) => server.close(resolve))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('http util integration', () => {
|
|
54
|
+
it('should send JSON body', async () => {
|
|
55
|
+
const result = await http.post({
|
|
56
|
+
url: `${baseUrl}/json`,
|
|
57
|
+
body: { hello: 'world' },
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(result.method).toBe('POST')
|
|
61
|
+
expect(result.body).toBe(JSON.stringify({ hello: 'world' }))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should send array JSON correctly', async () => {
|
|
65
|
+
const result = await http.post({
|
|
66
|
+
url: `${baseUrl}/array`,
|
|
67
|
+
body: [1, 2, 3],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(result.body).toBe(JSON.stringify([1, 2, 3]))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should send string body without stringify', async () => {
|
|
74
|
+
const result = await http.post({
|
|
75
|
+
url: `${baseUrl}/string`,
|
|
76
|
+
body: 'plain-text',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(result.body).toBe('plain-text')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should support base64 string', async () => {
|
|
83
|
+
const base64 = Buffer.from('hello').toString('base64')
|
|
84
|
+
|
|
85
|
+
const result = await http.post({
|
|
86
|
+
url: `${baseUrl}/base64`,
|
|
87
|
+
body: base64,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(result.body).toBe(base64)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should support URLSearchParams', async () => {
|
|
94
|
+
const params = new URLSearchParams({
|
|
95
|
+
grant_type: 'client_credentials',
|
|
96
|
+
scope: 'read',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const result = await http.post({
|
|
100
|
+
url: `${baseUrl}/form`,
|
|
101
|
+
body: params,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(result.body).toBe(params.toString())
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should support Buffer (binary)', async () => {
|
|
108
|
+
const buffer = Buffer.from('PDF-DATA-TEST')
|
|
109
|
+
|
|
110
|
+
const result = await http.post({
|
|
111
|
+
url: `${baseUrl}/buffer`,
|
|
112
|
+
body: buffer,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/pdf',
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(result.bodyLength).toBe(buffer.length)
|
|
119
|
+
expect(result.body).toBe(buffer.toString())
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should support Uint8Array (TypedArray)', async () => {
|
|
123
|
+
const uint8 = new Uint8Array([1, 2, 3, 4])
|
|
124
|
+
|
|
125
|
+
const result = await http.post({
|
|
126
|
+
url: `${baseUrl}/typed`,
|
|
127
|
+
body: uint8,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(result.bodyLength).toBe(uint8.length)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should support ArrayBuffer', async () => {
|
|
134
|
+
const buffer = new Uint8Array([5, 6, 7]).buffer
|
|
135
|
+
|
|
136
|
+
const result = await http.post({
|
|
137
|
+
url: `${baseUrl}/arraybuffer`,
|
|
138
|
+
body: buffer,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(result.bodyLength).toBe(3)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should support PUT with JSON', async () => {
|
|
145
|
+
const result = await http.put({
|
|
146
|
+
url: `${baseUrl}/put`,
|
|
147
|
+
body: { a: 1 },
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
expect(result.method).toBe('PUT')
|
|
151
|
+
expect(result.body).toBe(JSON.stringify({ a: 1 }))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should support PATCH with JSON', async () => {
|
|
155
|
+
const result = await http.patch({
|
|
156
|
+
url: `${baseUrl}/patch`,
|
|
157
|
+
body: { b: 2 },
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(result.method).toBe('PATCH')
|
|
161
|
+
expect(result.body).toBe(JSON.stringify({ b: 2 }))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should support DELETE with body', async () => {
|
|
165
|
+
const result = await http.deleteApi({
|
|
166
|
+
url: `${baseUrl}/delete`,
|
|
167
|
+
body: { remove: true },
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(result.method).toBe('DELETE')
|
|
171
|
+
expect(result.body).toBe(JSON.stringify({ remove: true }))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should not send body when undefined', async () => {
|
|
175
|
+
const result = await http.post({
|
|
176
|
+
url: `${baseUrl}/empty`,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(result.bodyLength).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should support GET request', async () => {
|
|
183
|
+
const result = await http.get({
|
|
184
|
+
url: `${baseUrl}/get`,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
expect(result.method).toBe('GET')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should support HEAD request', async () => {
|
|
191
|
+
const response = await http.head({
|
|
192
|
+
url: `${baseUrl}/head`,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(response.status).toBe(200)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should set JSON content-type by default', async () => {
|
|
199
|
+
const result = await http.post({
|
|
200
|
+
url: `${baseUrl}/json-header`,
|
|
201
|
+
body: { a: 1 },
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(result.headers['content-type']).toContain('application/json')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should allow overriding content-type', async () => {
|
|
208
|
+
const result = await http.post({
|
|
209
|
+
url: `${baseUrl}/override-header`,
|
|
210
|
+
body: { a: 1 },
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/custom+json',
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
expect(result.headers['content-type']).toContain('application/custom+json')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should preserve binary content-type', async () => {
|
|
220
|
+
const buffer = Buffer.from('file')
|
|
221
|
+
|
|
222
|
+
const result = await http.post({
|
|
223
|
+
url: `${baseUrl}/pdf`,
|
|
224
|
+
body: buffer,
|
|
225
|
+
headers: {
|
|
226
|
+
'Content-Type': 'application/pdf',
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
expect(result.headers['content-type']).toContain('application/pdf')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should not force JSON content-type for URLSearchParams', async () => {
|
|
234
|
+
const params = new URLSearchParams({ a: '1' })
|
|
235
|
+
|
|
236
|
+
const result = await http.post({
|
|
237
|
+
url: `${baseUrl}/urlencoded`,
|
|
238
|
+
body: params,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect(result.headers['content-type']).toContain(
|
|
242
|
+
'application/x-www-form-urlencoded',
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should not override multipart content-type for FormData', async () => {
|
|
247
|
+
const form = new FormData()
|
|
248
|
+
form.append('field', 'value')
|
|
249
|
+
|
|
250
|
+
const result = await http.post({
|
|
251
|
+
url: `${baseUrl}/multipart`,
|
|
252
|
+
body: form,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
expect(result.headers['content-type']).toContain('multipart/form-data')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should respect content-type for string body', async () => {
|
|
259
|
+
const result = await http.post({
|
|
260
|
+
url: `${baseUrl}/text`,
|
|
261
|
+
body: 'hello',
|
|
262
|
+
headers: {
|
|
263
|
+
'Content-Type': 'text/plain',
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(result.headers['content-type']).toContain('text/plain')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('GET should NOT send content-type header', async () => {
|
|
271
|
+
const result = await http.get({
|
|
272
|
+
url: `${baseUrl}/get-no-content-type`,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
expect(result.headers['content-type']).toBeUndefined()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should NOT add JSON content-type for Buffer without headers', async () => {
|
|
279
|
+
const buffer = Buffer.from('binary')
|
|
280
|
+
|
|
281
|
+
const result = await http.post({
|
|
282
|
+
url: `${baseUrl}/buffer-no-header`,
|
|
283
|
+
body: buffer,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
expect(result.headers['content-type']).toBeUndefined()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should NOT add JSON content-type for TypedArray', async () => {
|
|
290
|
+
const uint8 = new Uint8Array([1, 2, 3])
|
|
291
|
+
|
|
292
|
+
const result = await http.post({
|
|
293
|
+
url: `${baseUrl}/typed-no-header`,
|
|
294
|
+
body: uint8,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
expect(result.headers['content-type']).toBeUndefined()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should NOT add JSON content-type for ArrayBuffer', async () => {
|
|
301
|
+
const buffer = new Uint8Array([1, 2]).buffer
|
|
302
|
+
|
|
303
|
+
const result = await http.post({
|
|
304
|
+
url: `${baseUrl}/arraybuffer-no-header`,
|
|
305
|
+
body: buffer,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
expect(result.headers['content-type']).toBeUndefined()
|
|
309
|
+
})
|
|
310
|
+
})
|
|
@@ -166,14 +166,13 @@ describe('http client (native fetch)', () => {
|
|
|
166
166
|
})
|
|
167
167
|
|
|
168
168
|
describe('PUT with non-JSON body', () => {
|
|
169
|
-
it('should not stringify body
|
|
169
|
+
it('should not stringify string body', async () => {
|
|
170
170
|
const rawBody = 'plain text body'
|
|
171
171
|
mockFetch.mockResolvedValueOnce(createMockResponse({ body: 'OK' }))
|
|
172
172
|
|
|
173
173
|
await http.put({
|
|
174
174
|
url: 'http://test.com',
|
|
175
175
|
body: rawBody,
|
|
176
|
-
expectedType: ResponseType.text,
|
|
177
176
|
})
|
|
178
177
|
|
|
179
178
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
@@ -395,4 +394,104 @@ describe('edge cases', () => {
|
|
|
395
394
|
}),
|
|
396
395
|
)
|
|
397
396
|
})
|
|
397
|
+
|
|
398
|
+
it('should NOT add JSON content-type for Buffer', async () => {
|
|
399
|
+
const buffer = Buffer.from('binary')
|
|
400
|
+
|
|
401
|
+
mockFetch.mockResolvedValueOnce(
|
|
402
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
await http.post({
|
|
406
|
+
url: 'http://test.com',
|
|
407
|
+
body: buffer,
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
411
|
+
'http://test.com',
|
|
412
|
+
expect.objectContaining({
|
|
413
|
+
headers: expect.not.objectContaining({
|
|
414
|
+
'Content-Type': 'application/json',
|
|
415
|
+
}),
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('should NOT add JSON content-type for TypedArray', async () => {
|
|
421
|
+
const uint8 = new Uint8Array([1, 2, 3])
|
|
422
|
+
|
|
423
|
+
mockFetch.mockResolvedValueOnce(
|
|
424
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
await http.post({
|
|
428
|
+
url: 'http://test.com',
|
|
429
|
+
body: uint8,
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
433
|
+
'http://test.com',
|
|
434
|
+
expect.objectContaining({
|
|
435
|
+
headers: expect.not.objectContaining({
|
|
436
|
+
'Content-Type': 'application/json',
|
|
437
|
+
}),
|
|
438
|
+
}),
|
|
439
|
+
)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should NOT add JSON content-type for ArrayBuffer', async () => {
|
|
443
|
+
const buffer = new Uint8Array([1, 2]).buffer
|
|
444
|
+
|
|
445
|
+
mockFetch.mockResolvedValueOnce(
|
|
446
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
await http.post({
|
|
450
|
+
url: 'http://test.com',
|
|
451
|
+
body: buffer,
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
455
|
+
'http://test.com',
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
headers: expect.not.objectContaining({
|
|
458
|
+
'Content-Type': 'application/json',
|
|
459
|
+
}),
|
|
460
|
+
}),
|
|
461
|
+
)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('should include default Accept header in GET', async () => {
|
|
465
|
+
mockFetch.mockResolvedValueOnce(
|
|
466
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
await http.get({
|
|
470
|
+
url: 'http://test.com',
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
474
|
+
'http://test.com',
|
|
475
|
+
expect.objectContaining({
|
|
476
|
+
headers: expect.objectContaining({
|
|
477
|
+
Accept: 'application/json',
|
|
478
|
+
}),
|
|
479
|
+
}),
|
|
480
|
+
)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('HEAD should NOT include content-type header', async () => {
|
|
484
|
+
mockFetch.mockResolvedValueOnce(createMockResponse())
|
|
485
|
+
|
|
486
|
+
await http.head({
|
|
487
|
+
url: 'http://test.com',
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
491
|
+
'http://test.com',
|
|
492
|
+
expect.objectContaining({
|
|
493
|
+
headers: {},
|
|
494
|
+
}),
|
|
495
|
+
)
|
|
496
|
+
})
|
|
398
497
|
})
|
|
@@ -20,7 +20,7 @@ const log = pino({
|
|
|
20
20
|
level: 'silent',
|
|
21
21
|
})
|
|
22
22
|
// @ts-ignore
|
|
23
|
-
async function waitForRabbitConnection({ uri, log, timeoutMs =
|
|
23
|
+
async function waitForRabbitConnection({ uri, log, timeoutMs = 30000 }) {
|
|
24
24
|
const start = Date.now()
|
|
25
25
|
|
|
26
26
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
import { BaseRepository } from '../../src/postgresql/repositories/BaseRepository.js'
|
|
12
12
|
|
|
13
13
|
const PG_OPTIONS = {
|
|
14
|
-
port:
|
|
14
|
+
port: 5449,
|
|
15
15
|
db: 'testdb',
|
|
16
16
|
user: 'testuser',
|
|
17
17
|
pass: 'testpass',
|
|
18
|
-
containerName: 'postgres-base-repo-test-
|
|
18
|
+
containerName: 'postgres-base-repo-test-5449',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
import { TenantScopedRepository } from '../../src/postgresql/repositories/TenantScopedRepository.js'
|
|
12
12
|
|
|
13
13
|
const PG_OPTIONS = {
|
|
14
|
-
port:
|
|
14
|
+
port: 5448,
|
|
15
15
|
db: 'testdb',
|
|
16
16
|
user: 'testuser',
|
|
17
17
|
pass: 'testpass',
|
|
18
|
-
containerName: 'postgres-tenant-repo-test-
|
|
18
|
+
containerName: 'postgres-tenant-repo-test-5448',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
package/types/http/http.d.ts
CHANGED
|
@@ -45,8 +45,9 @@ export function head({
|
|
|
45
45
|
* put: (options: HttpPutOptions) => Promise<any>,
|
|
46
46
|
* post: (options: HttpPostOptions) => Promise<any>,
|
|
47
47
|
* patch: (options: HttpPatchOptions) => Promise<any>,
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* head: (options: HttpHeadOptions) => Promise<Response>,
|
|
49
|
+
* deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
50
|
+
*
|
|
50
51
|
* }}
|
|
51
52
|
*/
|
|
52
53
|
export const http: {
|
|
@@ -54,8 +55,8 @@ export const http: {
|
|
|
54
55
|
put: (options: HttpPutOptions) => Promise<any>
|
|
55
56
|
post: (options: HttpPostOptions) => Promise<any>
|
|
56
57
|
patch: (options: HttpPatchOptions) => Promise<any>
|
|
57
|
-
deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
58
58
|
head: (options: HttpHeadOptions) => Promise<Response>
|
|
59
|
+
deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
59
60
|
}
|
|
60
61
|
export type ResponseTypeValue = 'json' | 'xml' | 'text' | 'raw' | 'file'
|
|
61
62
|
export type HttpGetOptions = {
|