core-services-sdk 1.3.86 → 1.3.87

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.86",
3
+ "version": "1.3.87",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
package/src/http/http.js CHANGED
@@ -183,6 +183,45 @@ const getResponsePayload = async (response, responseType) => {
183
183
  }
184
184
  }
185
185
 
186
+ /**
187
+ * Normalizes the request body before sending it via fetch.
188
+ *
189
+ * Ensures backward compatibility while allowing additional body types.
190
+ * - undefined/null → no body sent
191
+ * - string → sent as-is
192
+ * - URLSearchParams → sent as-is (fetch serializes automatically)
193
+ * - FormData → sent as-is (fetch sets multipart boundary automatically)
194
+ * - ArrayBuffer / Blob → sent as-is
195
+ * - object / array → JSON.stringify applied and Content-Type ensured
196
+ *
197
+ * @param {any} body
198
+ * @param {Record<string,string>} headers
199
+ * @returns {{ body:any, headers:Record<string,string> }}
200
+ */
201
+ const prepareRequestBody = (body, headers = {}) => {
202
+ if (body === undefined || body === null) {
203
+ return { body: undefined, headers }
204
+ }
205
+
206
+ if (
207
+ typeof body === 'string' ||
208
+ body instanceof URLSearchParams ||
209
+ body instanceof FormData ||
210
+ body instanceof ArrayBuffer ||
211
+ body instanceof Blob
212
+ ) {
213
+ return { body, headers }
214
+ }
215
+
216
+ return {
217
+ body: JSON.stringify(body),
218
+ headers: {
219
+ 'Content-Type': 'application/json',
220
+ ...headers,
221
+ },
222
+ }
223
+ }
224
+
186
225
  /**
187
226
  * Sends an HTTP GET request.
188
227
  *
@@ -219,11 +258,15 @@ export const post = async ({
219
258
  extraParams = {},
220
259
  expectedType = ResponseType.json,
221
260
  }) => {
261
+ const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
262
+ body,
263
+ headers,
264
+ )
222
265
  const response = await fetch(url, {
223
266
  ...extraParams,
224
267
  method: HTTP_METHODS.POST,
225
- headers: { ...JSON_HEADER, ...headers },
226
- body: JSON.stringify(body),
268
+ headers: { ...JSON_HEADER, ...preparedHeaders },
269
+ body: preparedBody,
227
270
  })
228
271
  await checkStatus(response)
229
272
  return getResponsePayload(response, expectedType)
@@ -243,11 +286,15 @@ export const put = async ({
243
286
  extraParams = {},
244
287
  expectedType = ResponseType.json,
245
288
  }) => {
289
+ const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
290
+ body,
291
+ headers,
292
+ )
246
293
  const response = await fetch(url, {
247
294
  ...extraParams,
248
295
  method: HTTP_METHODS.PUT,
249
- headers: { ...JSON_HEADER, ...headers },
250
- body: expectedType === ResponseType.json ? JSON.stringify(body) : body,
296
+ headers: { ...JSON_HEADER, ...preparedHeaders },
297
+ body: preparedBody,
251
298
  })
252
299
  await checkStatus(response)
253
300
  return getResponsePayload(response, expectedType)
@@ -267,11 +314,15 @@ export const patch = async ({
267
314
  extraParams = {},
268
315
  expectedType = ResponseType.json,
269
316
  }) => {
317
+ const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
318
+ body,
319
+ headers,
320
+ )
270
321
  const response = await fetch(url, {
271
322
  ...extraParams,
272
323
  method: HTTP_METHODS.PATCH,
273
- headers: { ...JSON_HEADER, ...headers },
274
- body: JSON.stringify(body),
324
+ headers: { ...JSON_HEADER, ...preparedHeaders },
325
+ body: preparedBody,
275
326
  })
276
327
  await checkStatus(response)
277
328
  return getResponsePayload(response, expectedType)
@@ -291,11 +342,15 @@ export const deleteApi = async ({
291
342
  extraParams = {},
292
343
  expectedType = ResponseType.json,
293
344
  }) => {
345
+ const { body: preparedBody, headers: preparedHeaders } = prepareRequestBody(
346
+ body,
347
+ headers,
348
+ )
294
349
  const response = await fetch(url, {
295
350
  ...extraParams,
296
351
  method: HTTP_METHODS.DELETE,
297
- headers: { ...JSON_HEADER, ...headers },
298
- ...(body ? { body: JSON.stringify(body) } : {}),
352
+ headers: { ...JSON_HEADER, ...preparedHeaders },
353
+ ...(preparedBody ? { body: preparedBody } : {}),
299
354
  })
300
355
  await checkStatus(response)
301
356
  return getResponsePayload(response, expectedType)
@@ -328,8 +383,9 @@ export const head = async ({ url, headers = {}, extraParams = {} }) => {
328
383
  * put: (options: HttpPutOptions) => Promise<any>,
329
384
  * post: (options: HttpPostOptions) => Promise<any>,
330
385
  * patch: (options: HttpPatchOptions) => Promise<any>,
331
- * deleteApi: (options: HttpDeleteOptions) => Promise<any>,
332
- * head: (options: HttpHeadOptions) => Promise<Response>
386
+ * head: (options: HttpHeadOptions) => Promise<Response>,
387
+ * deleteApi: (options: HttpDeleteOptions) => Promise<any>
388
+ *
333
389
  * }}
334
390
  */
335
391
 
@@ -337,7 +393,7 @@ export const http = {
337
393
  get,
338
394
  put,
339
395
  post,
396
+ head,
340
397
  patch,
341
398
  deleteApi,
342
- head,
343
399
  }
@@ -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,141 @@
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 collectBody(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
+ resolve(Buffer.concat(chunks).toString())
19
+ })
20
+ })
21
+ }
22
+
23
+ beforeAll(async () => {
24
+ server = httpServer.createServer(async (req, res) => {
25
+ const body = await collectBody(req)
26
+
27
+ res.setHeader('content-type', 'application/json')
28
+
29
+ res.end(
30
+ JSON.stringify({
31
+ method: req.method,
32
+ headers: req.headers,
33
+ body,
34
+ }),
35
+ )
36
+ })
37
+
38
+ await new Promise((resolve) => {
39
+ server.listen(0, () => {
40
+ const { port } = server.address()
41
+ baseUrl = `http://localhost:${port}`
42
+ resolve()
43
+ })
44
+ })
45
+ })
46
+
47
+ afterAll(async () => {
48
+ await new Promise((resolve) => server.close(resolve))
49
+ })
50
+
51
+ describe('http util integration', () => {
52
+ it('should send JSON body', async () => {
53
+ const result = await http.post({
54
+ url: `${baseUrl}/json`,
55
+ body: { hello: 'world' },
56
+ })
57
+
58
+ expect(result.method).toBe('POST')
59
+ expect(result.body).toBe(JSON.stringify({ hello: 'world' }))
60
+ })
61
+
62
+ it('should send string body without stringify', async () => {
63
+ const result = await http.post({
64
+ url: `${baseUrl}/string`,
65
+ body: 'plain-text',
66
+ })
67
+
68
+ expect(result.body).toBe('plain-text')
69
+ })
70
+
71
+ it('should support base64 string', async () => {
72
+ const base64 = Buffer.from('hello').toString('base64')
73
+
74
+ const result = await http.post({
75
+ url: `${baseUrl}/base64`,
76
+ body: base64,
77
+ })
78
+
79
+ expect(result.body).toBe(base64)
80
+ })
81
+
82
+ it('should support URLSearchParams', async () => {
83
+ const params = new URLSearchParams({
84
+ grant_type: 'client_credentials',
85
+ scope: 'read',
86
+ })
87
+
88
+ const result = await http.post({
89
+ url: `${baseUrl}/form`,
90
+ body: params,
91
+ })
92
+
93
+ expect(result.body).toBe(params.toString())
94
+ })
95
+
96
+ it('should support PUT with JSON', async () => {
97
+ const result = await http.put({
98
+ url: `${baseUrl}/put`,
99
+ body: { a: 1 },
100
+ })
101
+
102
+ expect(result.method).toBe('PUT')
103
+ expect(result.body).toBe(JSON.stringify({ a: 1 }))
104
+ })
105
+
106
+ it('should support PATCH with JSON', async () => {
107
+ const result = await http.patch({
108
+ url: `${baseUrl}/patch`,
109
+ body: { b: 2 },
110
+ })
111
+
112
+ expect(result.method).toBe('PATCH')
113
+ expect(result.body).toBe(JSON.stringify({ b: 2 }))
114
+ })
115
+
116
+ it('should support DELETE with body', async () => {
117
+ const result = await http.deleteApi({
118
+ url: `${baseUrl}/delete`,
119
+ body: { remove: true },
120
+ })
121
+
122
+ expect(result.method).toBe('DELETE')
123
+ expect(result.body).toBe(JSON.stringify({ remove: true }))
124
+ })
125
+
126
+ it('should support GET request', async () => {
127
+ const result = await http.get({
128
+ url: `${baseUrl}/get`,
129
+ })
130
+
131
+ expect(result.method).toBe('GET')
132
+ })
133
+
134
+ it('should support HEAD request', async () => {
135
+ const response = await http.head({
136
+ url: `${baseUrl}/head`,
137
+ })
138
+
139
+ expect(response.status).toBe(200)
140
+ })
141
+ })
@@ -20,7 +20,7 @@ const log = pino({
20
20
  level: 'silent',
21
21
  })
22
22
  // @ts-ignore
23
- async function waitForRabbitConnection({ uri, log, timeoutMs = 10000 }) {
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: 5445,
14
+ port: 5449,
15
15
  db: 'testdb',
16
16
  user: 'testuser',
17
17
  pass: 'testpass',
18
- containerName: 'postgres-base-repo-test-5445',
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: 5446,
14
+ port: 5448,
15
15
  db: 'testdb',
16
16
  user: 'testuser',
17
17
  pass: 'testpass',
18
- containerName: 'postgres-tenant-repo-test-5446',
18
+ containerName: 'postgres-tenant-repo-test-5448',
19
19
  }
20
20
 
21
21
  const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
@@ -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
- * deleteApi: (options: HttpDeleteOptions) => Promise<any>,
49
- * head: (options: HttpHeadOptions) => Promise<Response>
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 = {
package/vitest.config.js CHANGED
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'
3
3
  export default defineConfig({
4
4
  test: {
5
5
  testTimeout: 30000,
6
- hookTimeout: 30000,
6
+ hookTimeout: 90000,
7
7
  exclude: [
8
8
  'node_modules',
9
9
  'types/**',