core-services-sdk 1.3.82 → 1.3.84

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.82",
3
+ "version": "1.3.84",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -1,7 +1,8 @@
1
1
  export const HTTP_METHODS = Object.freeze({
2
2
  GET: 'GET',
3
- POST: 'POST',
4
3
  PUT: 'PUT',
4
+ HEAD: 'HEAD',
5
+ POST: 'POST',
5
6
  PATCH: 'PATCH',
6
7
  DELETE: 'DELETE',
7
8
  })
package/src/http/http.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * @property {string} url - The URL to send the request to.
25
25
  * @property {any} body - The request body to send.
26
26
  * @property {Record<string, string>} [headers] - Optional HTTP headers.
27
+ * @property {RequestInit} [extraParams] - Additional fetch options.
27
28
  * @property {ResponseTypeValue} [expectedType] - Expected response type.
28
29
  */
29
30
 
@@ -32,6 +33,7 @@
32
33
  * @property {string} url - The URL to send the request to.
33
34
  * @property {any} body - The request body to send.
34
35
  * @property {Record<string, string>} [headers] - Optional HTTP headers.
36
+ * @property {RequestInit} [extraParams] - Additional fetch options.
35
37
  * @property {ResponseTypeValue} [expectedType] - Expected response type.
36
38
  */
37
39
 
@@ -40,6 +42,7 @@
40
42
  * @property {string} url - The URL to send the request to.
41
43
  * @property {any} body - The request body to send.
42
44
  * @property {Record<string, string>} [headers] - Optional HTTP headers.
45
+ * @property {RequestInit} [extraParams] - Additional fetch options.
43
46
  * @property {ResponseTypeValue} [expectedType] - Expected response type.
44
47
  */
45
48
 
@@ -48,9 +51,17 @@
48
51
  * @property {string} url - The URL to send the request to.
49
52
  * @property {any} [body] - Optional request body to send.
50
53
  * @property {Record<string, string>} [headers] - Optional HTTP headers.
54
+ * @property {RequestInit} [extraParams] - Additional fetch options.
51
55
  * @property {ResponseTypeValue} [expectedType] - Expected response type.
52
56
  */
53
57
 
58
+ /**
59
+ * @typedef {Object} HttpHeadOptions
60
+ * @property {string} url - The URL to send the request to.
61
+ * @property {Record<string, string>} [headers] - Optional HTTP headers.
62
+ * @property {RequestInit} [extraParams] - Additional fetch options.
63
+ */
64
+
54
65
  import httpStatus from 'http-status'
55
66
  import { parseStringPromise } from 'xml2js'
56
67
 
@@ -205,9 +216,11 @@ export const post = async ({
205
216
  url,
206
217
  body,
207
218
  headers = {},
219
+ extraParams = {},
208
220
  expectedType = ResponseType.json,
209
221
  }) => {
210
222
  const response = await fetch(url, {
223
+ ...extraParams,
211
224
  method: HTTP_METHODS.POST,
212
225
  headers: { ...JSON_HEADER, ...headers },
213
226
  body: JSON.stringify(body),
@@ -227,9 +240,11 @@ export const put = async ({
227
240
  url,
228
241
  body,
229
242
  headers = {},
243
+ extraParams = {},
230
244
  expectedType = ResponseType.json,
231
245
  }) => {
232
246
  const response = await fetch(url, {
247
+ ...extraParams,
233
248
  method: HTTP_METHODS.PUT,
234
249
  headers: { ...JSON_HEADER, ...headers },
235
250
  body: expectedType === ResponseType.json ? JSON.stringify(body) : body,
@@ -249,9 +264,11 @@ export const patch = async ({
249
264
  url,
250
265
  body,
251
266
  headers = {},
267
+ extraParams = {},
252
268
  expectedType = ResponseType.json,
253
269
  }) => {
254
270
  const response = await fetch(url, {
271
+ ...extraParams,
255
272
  method: HTTP_METHODS.PATCH,
256
273
  headers: { ...JSON_HEADER, ...headers },
257
274
  body: JSON.stringify(body),
@@ -271,9 +288,11 @@ export const deleteApi = async ({
271
288
  url,
272
289
  body,
273
290
  headers = {},
291
+ extraParams = {},
274
292
  expectedType = ResponseType.json,
275
293
  }) => {
276
294
  const response = await fetch(url, {
295
+ ...extraParams,
277
296
  method: HTTP_METHODS.DELETE,
278
297
  headers: { ...JSON_HEADER, ...headers },
279
298
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -282,6 +301,25 @@ export const deleteApi = async ({
282
301
  return getResponsePayload(response, expectedType)
283
302
  }
284
303
 
304
+ /**
305
+ * Sends an HTTP HEAD request.
306
+ *
307
+ * @param {HttpHeadOptions} options - The request options.
308
+ * @returns {Promise<Response>} The raw fetch response (headers only).
309
+ * @throws {HttpError} If the response status is not successful.
310
+ */
311
+ export const head = async ({ url, headers = {}, extraParams = {} }) => {
312
+ const response = await fetch(url, {
313
+ ...extraParams,
314
+ method: HTTP_METHODS.HEAD,
315
+ headers: { ...JSON_HEADER, ...headers },
316
+ })
317
+
318
+ await checkStatus(response)
319
+
320
+ return response
321
+ }
322
+
285
323
  /**
286
324
  * Consolidated HTTP client with methods for common HTTP operations.
287
325
  *
@@ -290,13 +328,16 @@ export const deleteApi = async ({
290
328
  * put: (options: HttpPutOptions) => Promise<any>,
291
329
  * post: (options: HttpPostOptions) => Promise<any>,
292
330
  * patch: (options: HttpPatchOptions) => Promise<any>,
293
- * deleteApi: (options: HttpDeleteOptions) => Promise<any>
331
+ * deleteApi: (options: HttpDeleteOptions) => Promise<any>,
332
+ * head: (options: HttpHeadOptions) => Promise<Response>
294
333
  * }}
295
334
  */
335
+
296
336
  export const http = {
297
337
  get,
298
338
  put,
299
339
  post,
300
340
  patch,
301
341
  deleteApi,
342
+ head,
302
343
  }
@@ -6,9 +6,10 @@ import { HTTP_METHODS } from '../../src/http/http-method.js'
6
6
  describe('HTTP_METHODS constant', () => {
7
7
  it('should include all standard HTTP methods', () => {
8
8
  expect(HTTP_METHODS).toEqual({
9
+ PUT: 'PUT',
9
10
  GET: 'GET',
11
+ HEAD: 'HEAD',
10
12
  POST: 'POST',
11
- PUT: 'PUT',
12
13
  PATCH: 'PATCH',
13
14
  DELETE: 'DELETE',
14
15
  })
@@ -20,6 +20,7 @@ const createMockResponse = ({
20
20
  statusText,
21
21
  text: vi.fn().mockResolvedValue(body),
22
22
  headers: {
23
+ // @ts-ignore
23
24
  get: vi.fn().mockImplementation((k) => headers[k]),
24
25
  },
25
26
  }
@@ -185,3 +186,213 @@ describe('http client (native fetch)', () => {
185
186
  })
186
187
  })
187
188
  })
189
+
190
+ describe('extraParams support', () => {
191
+ it('should pass extraParams to fetch in GET', async () => {
192
+ mockFetch.mockResolvedValueOnce(
193
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
194
+ )
195
+
196
+ const controller = new AbortController()
197
+
198
+ await http.get({
199
+ url: 'http://test.com',
200
+ extraParams: {
201
+ signal: controller.signal,
202
+ redirect: 'follow',
203
+ },
204
+ })
205
+
206
+ expect(mockFetch).toHaveBeenCalledWith(
207
+ 'http://test.com',
208
+ expect.objectContaining({
209
+ method: HTTP_METHODS.GET,
210
+ signal: controller.signal,
211
+ redirect: 'follow',
212
+ }),
213
+ )
214
+ })
215
+
216
+ it('should pass extraParams to fetch in POST', async () => {
217
+ mockFetch.mockResolvedValueOnce(
218
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
219
+ )
220
+
221
+ await http.post({
222
+ url: 'http://test.com',
223
+ body: { a: 1 },
224
+ extraParams: {
225
+ credentials: 'include',
226
+ },
227
+ })
228
+
229
+ expect(mockFetch).toHaveBeenCalledWith(
230
+ 'http://test.com',
231
+ expect.objectContaining({
232
+ method: HTTP_METHODS.POST,
233
+ credentials: 'include',
234
+ }),
235
+ )
236
+ })
237
+
238
+ it('should pass extraParams to fetch in DELETE', async () => {
239
+ mockFetch.mockResolvedValueOnce(
240
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
241
+ )
242
+
243
+ await http.deleteApi({
244
+ url: 'http://test.com',
245
+ extraParams: {
246
+ keepalive: true,
247
+ },
248
+ })
249
+
250
+ expect(mockFetch).toHaveBeenCalledWith(
251
+ 'http://test.com',
252
+ expect.objectContaining({
253
+ method: HTTP_METHODS.DELETE,
254
+ keepalive: true,
255
+ }),
256
+ )
257
+ })
258
+ })
259
+
260
+ describe('HEAD', () => {
261
+ it('should send a HEAD request and return response object', async () => {
262
+ const mockResponse = createMockResponse({
263
+ status: 200,
264
+ headers: { 'content-length': '1234' },
265
+ })
266
+
267
+ mockFetch.mockResolvedValueOnce(mockResponse)
268
+
269
+ const result = await http.head({
270
+ url: 'http://test.com',
271
+ })
272
+
273
+ expect(result).toBe(mockResponse)
274
+
275
+ expect(mockFetch).toHaveBeenCalledWith(
276
+ 'http://test.com',
277
+ expect.objectContaining({
278
+ method: HTTP_METHODS.HEAD,
279
+ }),
280
+ )
281
+ })
282
+
283
+ it('should expose response headers', async () => {
284
+ const mockResponse = createMockResponse({
285
+ headers: { etag: 'abc123' },
286
+ })
287
+
288
+ mockFetch.mockResolvedValueOnce(mockResponse)
289
+
290
+ const result = await http.head({
291
+ url: 'http://test.com',
292
+ })
293
+
294
+ expect(result.headers.get('etag')).toBe('abc123')
295
+ })
296
+
297
+ it('should throw HttpError on non-2xx status', async () => {
298
+ mockFetch.mockResolvedValueOnce(
299
+ createMockResponse({
300
+ status: 500,
301
+ body: 'server error',
302
+ }),
303
+ )
304
+
305
+ await expect(
306
+ http.head({
307
+ url: 'http://test.com',
308
+ }),
309
+ ).rejects.toThrow(HttpError)
310
+ })
311
+ })
312
+
313
+ describe('edge cases', () => {
314
+ it('should fallback to text when JSON parsing fails', async () => {
315
+ const invalidJson = '{invalid json'
316
+
317
+ mockFetch.mockResolvedValueOnce(createMockResponse({ body: invalidJson }))
318
+
319
+ const result = await http.get({
320
+ url: 'http://test.com',
321
+ expectedType: ResponseType.json,
322
+ })
323
+
324
+ expect(result).toBe(invalidJson)
325
+ })
326
+
327
+ it('should fallback to text when XML parsing fails', async () => {
328
+ const invalidXml = '<note><invalid></note>'
329
+
330
+ mockFetch.mockResolvedValueOnce(createMockResponse({ body: invalidXml }))
331
+
332
+ const result = await http.get({
333
+ url: 'http://test.com',
334
+ expectedType: ResponseType.xml,
335
+ })
336
+
337
+ expect(result).toBe(invalidXml)
338
+ })
339
+
340
+ it('should allow overriding headers', async () => {
341
+ mockFetch.mockResolvedValueOnce(
342
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
343
+ )
344
+
345
+ await http.get({
346
+ url: 'http://test.com',
347
+ headers: {
348
+ Authorization: 'Bearer token123',
349
+ },
350
+ })
351
+
352
+ expect(mockFetch).toHaveBeenCalledWith(
353
+ 'http://test.com',
354
+ expect.objectContaining({
355
+ headers: expect.objectContaining({
356
+ Authorization: 'Bearer token123',
357
+ }),
358
+ }),
359
+ )
360
+ })
361
+
362
+ it('should send DELETE request without body', async () => {
363
+ mockFetch.mockResolvedValueOnce(
364
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
365
+ )
366
+
367
+ await http.deleteApi({
368
+ url: 'http://test.com',
369
+ })
370
+
371
+ expect(mockFetch).toHaveBeenCalledWith(
372
+ 'http://test.com',
373
+ expect.objectContaining({
374
+ method: HTTP_METHODS.DELETE,
375
+ }),
376
+ )
377
+ })
378
+
379
+ it('should include default JSON content-type header', async () => {
380
+ mockFetch.mockResolvedValueOnce(
381
+ createMockResponse({ body: JSON.stringify({ ok: true }) }),
382
+ )
383
+
384
+ await http.post({
385
+ url: 'http://test.com',
386
+ body: { a: 1 },
387
+ })
388
+
389
+ expect(mockFetch).toHaveBeenCalledWith(
390
+ 'http://test.com',
391
+ expect.objectContaining({
392
+ headers: expect.objectContaining({
393
+ 'Content-Type': 'application/json',
394
+ }),
395
+ }),
396
+ )
397
+ })
398
+ })
@@ -1,7 +1,8 @@
1
1
  export const HTTP_METHODS: Readonly<{
2
2
  GET: 'GET'
3
- POST: 'POST'
4
3
  PUT: 'PUT'
4
+ HEAD: 'HEAD'
5
+ POST: 'POST'
5
6
  PATCH: 'PATCH'
6
7
  DELETE: 'DELETE'
7
8
  }>
@@ -3,60 +3,178 @@ export function get({
3
3
  headers,
4
4
  extraParams,
5
5
  expectedType,
6
- }: {
7
- url: any
8
- headers?: {}
9
- extraParams?: {}
10
- expectedType?: 'json'
11
- }): Promise<any>
6
+ }: HttpGetOptions): Promise<any>
12
7
  export function post({
13
8
  url,
14
9
  body,
15
10
  headers,
11
+ extraParams,
16
12
  expectedType,
17
- }: {
18
- url: any
19
- body: any
20
- headers?: {}
21
- expectedType?: 'json'
22
- }): Promise<any>
13
+ }: HttpPostOptions): Promise<any>
23
14
  export function put({
24
15
  url,
25
16
  body,
26
17
  headers,
18
+ extraParams,
27
19
  expectedType,
28
- }: {
29
- url: any
30
- body: any
31
- headers?: {}
32
- expectedType?: 'json'
33
- }): Promise<any>
20
+ }: HttpPutOptions): Promise<any>
34
21
  export function patch({
35
22
  url,
36
23
  body,
37
24
  headers,
25
+ extraParams,
38
26
  expectedType,
39
- }: {
40
- url: any
41
- body: any
42
- headers?: {}
43
- expectedType?: 'json'
44
- }): Promise<any>
27
+ }: HttpPatchOptions): Promise<any>
45
28
  export function deleteApi({
46
29
  url,
47
30
  body,
48
31
  headers,
32
+ extraParams,
49
33
  expectedType,
50
- }: {
51
- url: any
34
+ }: HttpDeleteOptions): Promise<any>
35
+ export function head({
36
+ url,
37
+ headers,
38
+ extraParams,
39
+ }: HttpHeadOptions): Promise<Response>
40
+ /**
41
+ * Consolidated HTTP client with methods for common HTTP operations.
42
+ *
43
+ * @type {{
44
+ * get: (options: HttpGetOptions) => Promise<any>,
45
+ * put: (options: HttpPutOptions) => Promise<any>,
46
+ * post: (options: HttpPostOptions) => Promise<any>,
47
+ * patch: (options: HttpPatchOptions) => Promise<any>,
48
+ * deleteApi: (options: HttpDeleteOptions) => Promise<any>,
49
+ * head: (options: HttpHeadOptions) => Promise<Response>
50
+ * }}
51
+ */
52
+ export const http: {
53
+ get: (options: HttpGetOptions) => Promise<any>
54
+ put: (options: HttpPutOptions) => Promise<any>
55
+ post: (options: HttpPostOptions) => Promise<any>
56
+ patch: (options: HttpPatchOptions) => Promise<any>
57
+ deleteApi: (options: HttpDeleteOptions) => Promise<any>
58
+ head: (options: HttpHeadOptions) => Promise<Response>
59
+ }
60
+ export type ResponseTypeValue = 'json' | 'xml' | 'text' | 'raw' | 'file'
61
+ export type HttpGetOptions = {
62
+ /**
63
+ * - The URL to send the request to.
64
+ */
65
+ url: string
66
+ /**
67
+ * - Optional HTTP headers.
68
+ */
69
+ headers?: Record<string, string>
70
+ /**
71
+ * - Additional fetch options.
72
+ */
73
+ extraParams?: RequestInit
74
+ /**
75
+ * - Expected response type.
76
+ */
77
+ expectedType?: ResponseTypeValue
78
+ }
79
+ export type HttpPostOptions = {
80
+ /**
81
+ * - The URL to send the request to.
82
+ */
83
+ url: string
84
+ /**
85
+ * - The request body to send.
86
+ */
87
+ body: any
88
+ /**
89
+ * - Optional HTTP headers.
90
+ */
91
+ headers?: Record<string, string>
92
+ /**
93
+ * - Additional fetch options.
94
+ */
95
+ extraParams?: RequestInit
96
+ /**
97
+ * - Expected response type.
98
+ */
99
+ expectedType?: ResponseTypeValue
100
+ }
101
+ export type HttpPutOptions = {
102
+ /**
103
+ * - The URL to send the request to.
104
+ */
105
+ url: string
106
+ /**
107
+ * - The request body to send.
108
+ */
52
109
  body: any
53
- headers?: {}
54
- expectedType?: 'json'
55
- }): Promise<any>
56
- export namespace http {
57
- export { get }
58
- export { put }
59
- export { post }
60
- export { patch }
61
- export { deleteApi }
110
+ /**
111
+ * - Optional HTTP headers.
112
+ */
113
+ headers?: Record<string, string>
114
+ /**
115
+ * - Additional fetch options.
116
+ */
117
+ extraParams?: RequestInit
118
+ /**
119
+ * - Expected response type.
120
+ */
121
+ expectedType?: ResponseTypeValue
122
+ }
123
+ export type HttpPatchOptions = {
124
+ /**
125
+ * - The URL to send the request to.
126
+ */
127
+ url: string
128
+ /**
129
+ * - The request body to send.
130
+ */
131
+ body: any
132
+ /**
133
+ * - Optional HTTP headers.
134
+ */
135
+ headers?: Record<string, string>
136
+ /**
137
+ * - Additional fetch options.
138
+ */
139
+ extraParams?: RequestInit
140
+ /**
141
+ * - Expected response type.
142
+ */
143
+ expectedType?: ResponseTypeValue
144
+ }
145
+ export type HttpDeleteOptions = {
146
+ /**
147
+ * - The URL to send the request to.
148
+ */
149
+ url: string
150
+ /**
151
+ * - Optional request body to send.
152
+ */
153
+ body?: any
154
+ /**
155
+ * - Optional HTTP headers.
156
+ */
157
+ headers?: Record<string, string>
158
+ /**
159
+ * - Additional fetch options.
160
+ */
161
+ extraParams?: RequestInit
162
+ /**
163
+ * - Expected response type.
164
+ */
165
+ expectedType?: ResponseTypeValue
166
+ }
167
+ export type HttpHeadOptions = {
168
+ /**
169
+ * - The URL to send the request to.
170
+ */
171
+ url: string
172
+ /**
173
+ * - Optional HTTP headers.
174
+ */
175
+ headers?: Record<string, string>
176
+ /**
177
+ * - Additional fetch options.
178
+ */
179
+ extraParams?: RequestInit
62
180
  }
@@ -5,4 +5,6 @@ export * from './filters/apply-filter.js'
5
5
  export * from './modifiers/apply-order-by.js'
6
6
  export * from './start-stop-postgres-docker.js'
7
7
  export * from './modifiers/apply-pagination.js'
8
+ export * from './repositories/BaseRepository.js'
8
9
  export * from './filters/apply-filter-snake-case.js'
10
+ export * from './repositories/TenantScopedRepository.js'
@@ -69,6 +69,7 @@ export function sqlPaginate({
69
69
  limit,
70
70
  filter,
71
71
  snakeCase,
72
+ distinctOn,
72
73
  }: {
73
74
  baseQuery: import('knex').Knex.QueryBuilder
74
75
  filter?: any
@@ -0,0 +1,50 @@
1
+ /**
2
+ * BaseRepository
3
+ *
4
+ * Provides:
5
+ * - tableName getter (static + instance support)
6
+ * - baseQuery(options)
7
+ * - constructor-level query shaping (optional)
8
+ * - generic find() with pagination
9
+ *
10
+ * Does NOT enforce tenant isolation.
11
+ */
12
+ export class BaseRepository {
13
+ constructor({ db, log, baseQueryBuilder }?: {})
14
+ db: any
15
+ log: any
16
+ _baseQueryBuilder: any
17
+ /**
18
+ * Each concrete repository must define:
19
+ * static tableName = 'table_name'
20
+ */
21
+ get tableName(): any
22
+ /**
23
+ * Builds the base knex query.
24
+ * Applies constructor-level baseQueryBuilder if provided.
25
+ */
26
+ baseQuery(options: {}, params: any): any
27
+ /**
28
+ * Generic paginated find
29
+ */
30
+ find({
31
+ page,
32
+ limit,
33
+ filter,
34
+ orderBy,
35
+ options,
36
+ mapRow,
37
+ ...restParams
38
+ }: {
39
+ [x: string]: any
40
+ page?: number
41
+ limit?: number
42
+ filter?: {}
43
+ orderBy?: {
44
+ column: string
45
+ direction: string
46
+ }
47
+ options?: {}
48
+ mapRow?: (row: any) => any
49
+ }): Promise<any>
50
+ }
@@ -0,0 +1,11 @@
1
+ export class TenantScopedRepository extends BaseRepository {
2
+ /**
3
+ * Extracts and validates tenantId from filter
4
+ */
5
+ getRequiredTenantId(filter?: {}): any
6
+ /**
7
+ * Overrides find to enforce tenant presence
8
+ */
9
+ find(params?: {}): Promise<any>
10
+ }
11
+ import { BaseRepository } from './BaseRepository.js'