core-services-sdk 1.3.81 → 1.3.83
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 +12 -0
- package/src/postgresql/index.js +2 -0
- package/src/postgresql/pagination/paginate.js +4 -1
- package/src/postgresql/repositories/BaseRepository.js +95 -0
- package/src/postgresql/repositories/TenantScopedRepository.js +40 -0
- package/tests/http/http.unit.test.js +70 -0
- package/tests/repositories/BaseRepository.test.js +104 -0
- package/tests/repositories/TenantScopedRepository.test.js +44 -0
- package/types/http/http.d.ts +132 -35
- package/types/postgresql/index.d.ts +2 -0
- package/types/postgresql/pagination/paginate.d.ts +1 -0
- package/types/postgresql/repositories/BaseRepository.d.ts +50 -0
- package/types/postgresql/repositories/TenantScopedRepository.d.ts +11 -0
package/package.json
CHANGED
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,6 +51,7 @@
|
|
|
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
|
|
|
@@ -205,9 +209,11 @@ export const post = async ({
|
|
|
205
209
|
url,
|
|
206
210
|
body,
|
|
207
211
|
headers = {},
|
|
212
|
+
extraParams = {},
|
|
208
213
|
expectedType = ResponseType.json,
|
|
209
214
|
}) => {
|
|
210
215
|
const response = await fetch(url, {
|
|
216
|
+
...extraParams,
|
|
211
217
|
method: HTTP_METHODS.POST,
|
|
212
218
|
headers: { ...JSON_HEADER, ...headers },
|
|
213
219
|
body: JSON.stringify(body),
|
|
@@ -227,9 +233,11 @@ export const put = async ({
|
|
|
227
233
|
url,
|
|
228
234
|
body,
|
|
229
235
|
headers = {},
|
|
236
|
+
extraParams = {},
|
|
230
237
|
expectedType = ResponseType.json,
|
|
231
238
|
}) => {
|
|
232
239
|
const response = await fetch(url, {
|
|
240
|
+
...extraParams,
|
|
233
241
|
method: HTTP_METHODS.PUT,
|
|
234
242
|
headers: { ...JSON_HEADER, ...headers },
|
|
235
243
|
body: expectedType === ResponseType.json ? JSON.stringify(body) : body,
|
|
@@ -249,9 +257,11 @@ export const patch = async ({
|
|
|
249
257
|
url,
|
|
250
258
|
body,
|
|
251
259
|
headers = {},
|
|
260
|
+
extraParams = {},
|
|
252
261
|
expectedType = ResponseType.json,
|
|
253
262
|
}) => {
|
|
254
263
|
const response = await fetch(url, {
|
|
264
|
+
...extraParams,
|
|
255
265
|
method: HTTP_METHODS.PATCH,
|
|
256
266
|
headers: { ...JSON_HEADER, ...headers },
|
|
257
267
|
body: JSON.stringify(body),
|
|
@@ -271,9 +281,11 @@ export const deleteApi = async ({
|
|
|
271
281
|
url,
|
|
272
282
|
body,
|
|
273
283
|
headers = {},
|
|
284
|
+
extraParams = {},
|
|
274
285
|
expectedType = ResponseType.json,
|
|
275
286
|
}) => {
|
|
276
287
|
const response = await fetch(url, {
|
|
288
|
+
...extraParams,
|
|
277
289
|
method: HTTP_METHODS.DELETE,
|
|
278
290
|
headers: { ...JSON_HEADER, ...headers },
|
|
279
291
|
...(body ? { body: JSON.stringify(body) } : {}),
|
package/src/postgresql/index.js
CHANGED
|
@@ -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'
|
|
@@ -75,6 +75,7 @@ export async function sqlPaginate({
|
|
|
75
75
|
limit = 10,
|
|
76
76
|
filter = {},
|
|
77
77
|
snakeCase = true,
|
|
78
|
+
distinctOn,
|
|
78
79
|
}) {
|
|
79
80
|
const listQuery = baseQuery.clone()
|
|
80
81
|
const countQuery = baseQuery.clone()
|
|
@@ -89,7 +90,9 @@ export async function sqlPaginate({
|
|
|
89
90
|
|
|
90
91
|
const [rows, countResult] = await Promise.all([
|
|
91
92
|
listQuery.select('*'),
|
|
92
|
-
|
|
93
|
+
distinctOn
|
|
94
|
+
? countQuery.countDistinct(`${distinctOn} as count`).first()
|
|
95
|
+
: countQuery.count('* as count').first(),
|
|
93
96
|
])
|
|
94
97
|
|
|
95
98
|
const totalCount = normalizeNumberOrDefault(countResult?.count || 0)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { sqlPaginate } from '../pagination/paginate.js'
|
|
2
|
+
import { toSnakeCase } from '../../core/case-mapper.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BaseRepository
|
|
6
|
+
*
|
|
7
|
+
* Provides:
|
|
8
|
+
* - tableName getter (static + instance support)
|
|
9
|
+
* - baseQuery(options)
|
|
10
|
+
* - constructor-level query shaping (optional)
|
|
11
|
+
* - generic find() with pagination
|
|
12
|
+
*
|
|
13
|
+
* Does NOT enforce tenant isolation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class BaseRepository {
|
|
17
|
+
constructor({ db, log, baseQueryBuilder } = {}) {
|
|
18
|
+
if (!db) {
|
|
19
|
+
throw new Error('BaseRepository requires db instance')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.db = db
|
|
23
|
+
this.log = log
|
|
24
|
+
this._baseQueryBuilder = baseQueryBuilder
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Each concrete repository must define:
|
|
29
|
+
* static tableName = 'table_name'
|
|
30
|
+
*/
|
|
31
|
+
get tableName() {
|
|
32
|
+
if (!this.constructor.tableName) {
|
|
33
|
+
throw new Error(`tableName not defined for ${this.constructor.name}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.constructor.tableName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds the base knex query.
|
|
41
|
+
* Applies constructor-level baseQueryBuilder if provided.
|
|
42
|
+
*/
|
|
43
|
+
baseQuery(options = {}, params) {
|
|
44
|
+
const { trx } = options
|
|
45
|
+
const connection = trx || this.db
|
|
46
|
+
|
|
47
|
+
const qb = connection(this.tableName)
|
|
48
|
+
|
|
49
|
+
if (this._baseQueryBuilder) {
|
|
50
|
+
// Pass full params for optional dynamic shaping
|
|
51
|
+
return this._baseQueryBuilder(qb, params)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return qb
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generic paginated find
|
|
59
|
+
*/
|
|
60
|
+
async find({
|
|
61
|
+
page = 1,
|
|
62
|
+
limit = 10,
|
|
63
|
+
filter = {},
|
|
64
|
+
orderBy = { column: 'created_at', direction: 'desc' },
|
|
65
|
+
options = {},
|
|
66
|
+
mapRow = (row) => row,
|
|
67
|
+
...restParams
|
|
68
|
+
}) {
|
|
69
|
+
try {
|
|
70
|
+
const qb = this.baseQuery(options, {
|
|
71
|
+
page,
|
|
72
|
+
limit,
|
|
73
|
+
filter,
|
|
74
|
+
orderBy,
|
|
75
|
+
options,
|
|
76
|
+
...restParams,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return await sqlPaginate({
|
|
80
|
+
page,
|
|
81
|
+
limit,
|
|
82
|
+
orderBy,
|
|
83
|
+
baseQuery: qb,
|
|
84
|
+
mapRow,
|
|
85
|
+
filter: toSnakeCase(filter),
|
|
86
|
+
})
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (this.log) {
|
|
89
|
+
this.log.error({ error }, `Error finding from ${this.tableName}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw error
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TenantScopedRepository
|
|
3
|
+
*
|
|
4
|
+
* Extends BaseRepository
|
|
5
|
+
* Enforces tenantId presence inside filter.
|
|
6
|
+
*
|
|
7
|
+
* Does NOT modify filter.
|
|
8
|
+
* Does NOT rebuild filter.
|
|
9
|
+
* Only validates existence.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BaseRepository } from './BaseRepository.js'
|
|
13
|
+
|
|
14
|
+
export class TenantScopedRepository extends BaseRepository {
|
|
15
|
+
/**
|
|
16
|
+
* Extracts and validates tenantId from filter
|
|
17
|
+
*/
|
|
18
|
+
getRequiredTenantId(filter = {}) {
|
|
19
|
+
const { tenantId } = filter
|
|
20
|
+
|
|
21
|
+
if (!tenantId) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Scoped repository (${this.tableName}), tenantId is required`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return tenantId
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Overrides find to enforce tenant presence
|
|
32
|
+
*/
|
|
33
|
+
async find(params = {}) {
|
|
34
|
+
const { filter = {} } = params
|
|
35
|
+
|
|
36
|
+
this.getRequiredTenantId(filter)
|
|
37
|
+
|
|
38
|
+
return super.find(params)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -185,3 +185,73 @@ describe('http client (native fetch)', () => {
|
|
|
185
185
|
})
|
|
186
186
|
})
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
describe('extraParams support', () => {
|
|
190
|
+
it('should pass extraParams to fetch in GET', async () => {
|
|
191
|
+
mockFetch.mockResolvedValueOnce(
|
|
192
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const controller = new AbortController()
|
|
196
|
+
|
|
197
|
+
await http.get({
|
|
198
|
+
url: 'http://test.com',
|
|
199
|
+
extraParams: {
|
|
200
|
+
signal: controller.signal,
|
|
201
|
+
redirect: 'follow',
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
206
|
+
'http://test.com',
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
method: HTTP_METHODS.GET,
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
redirect: 'follow',
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should pass extraParams to fetch in POST', async () => {
|
|
216
|
+
mockFetch.mockResolvedValueOnce(
|
|
217
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
await http.post({
|
|
221
|
+
url: 'http://test.com',
|
|
222
|
+
body: { a: 1 },
|
|
223
|
+
extraParams: {
|
|
224
|
+
credentials: 'include',
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
229
|
+
'http://test.com',
|
|
230
|
+
expect.objectContaining({
|
|
231
|
+
method: HTTP_METHODS.POST,
|
|
232
|
+
credentials: 'include',
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should pass extraParams to fetch in DELETE', async () => {
|
|
238
|
+
mockFetch.mockResolvedValueOnce(
|
|
239
|
+
createMockResponse({ body: JSON.stringify({ ok: true }) }),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
await http.deleteApi({
|
|
243
|
+
url: 'http://test.com',
|
|
244
|
+
extraParams: {
|
|
245
|
+
keepalive: true,
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
250
|
+
'http://test.com',
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
method: HTTP_METHODS.DELETE,
|
|
253
|
+
keepalive: true,
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { BaseRepository } from '../../src/postgresql/repositories/BaseRepository.js'
|
|
3
|
+
|
|
4
|
+
// 🔥 mock dependencies
|
|
5
|
+
vi.mock('../../src/postgresql/pagination/paginate.js', () => ({
|
|
6
|
+
sqlPaginate: vi.fn().mockResolvedValue({ page: 1 }),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
vi.mock('../../filters/apply-filter-snake-case.js', () => ({
|
|
10
|
+
applyFilterSnakeCase: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
vi.mock('../../filters/apply-filter.js', () => ({
|
|
14
|
+
applyFilter: vi.fn(),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('../../core/normalize-premitives-types-or-default.js', () => ({
|
|
18
|
+
normalizeNumberOrDefault: vi.fn((v) => Number(v)),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
// import AFTER mocks
|
|
22
|
+
import { sqlPaginate } from '../../src/postgresql/pagination/paginate.js'
|
|
23
|
+
import { toSnakeCase } from '../../src/core/case-mapper.js'
|
|
24
|
+
|
|
25
|
+
describe('BaseRepository', () => {
|
|
26
|
+
let dbMock
|
|
27
|
+
let logMock
|
|
28
|
+
|
|
29
|
+
class TestRepo extends BaseRepository {
|
|
30
|
+
static tableName = 'test_table'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
dbMock = vi.fn(() => ({
|
|
35
|
+
clone: vi.fn(),
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
logMock = {
|
|
39
|
+
error: vi.fn(),
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('throws if db not provided', () => {
|
|
44
|
+
expect(() => new BaseRepository()).toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns correct tableName', () => {
|
|
48
|
+
const repo = new TestRepo({ db: dbMock })
|
|
49
|
+
expect(repo.tableName).toBe('test_table')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('baseQuery uses db when no trx', () => {
|
|
53
|
+
const repo = new TestRepo({ db: dbMock })
|
|
54
|
+
|
|
55
|
+
repo.baseQuery()
|
|
56
|
+
|
|
57
|
+
expect(dbMock).toHaveBeenCalledWith('test_table')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('baseQuery uses trx when provided', () => {
|
|
61
|
+
const trxMock = vi.fn()
|
|
62
|
+
const repo = new TestRepo({ db: dbMock })
|
|
63
|
+
|
|
64
|
+
repo.baseQuery({ trx: trxMock })
|
|
65
|
+
|
|
66
|
+
expect(trxMock).toHaveBeenCalledWith('test_table')
|
|
67
|
+
expect(dbMock).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('baseQuery applies baseQueryBuilder', () => {
|
|
71
|
+
const builderMock = vi.fn((qb) => qb)
|
|
72
|
+
const repo = new TestRepo({
|
|
73
|
+
db: dbMock,
|
|
74
|
+
baseQueryBuilder: builderMock,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
repo.baseQuery({}, { some: 'param' })
|
|
78
|
+
|
|
79
|
+
expect(builderMock).toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('find calls sqlPaginate with correct params', async () => {
|
|
83
|
+
const repo = new TestRepo({ db: dbMock })
|
|
84
|
+
|
|
85
|
+
await repo.find({
|
|
86
|
+
page: 2,
|
|
87
|
+
limit: 5,
|
|
88
|
+
filter: { a: 1 },
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(sqlPaginate).toHaveBeenCalled()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('logs error if sqlPaginate throws', async () => {
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
sqlPaginate.mockRejectedValueOnce(new Error('fail'))
|
|
97
|
+
|
|
98
|
+
const repo = new TestRepo({ db: dbMock, log: logMock })
|
|
99
|
+
|
|
100
|
+
await expect(repo.find({ filter: {} })).rejects.toThrow()
|
|
101
|
+
|
|
102
|
+
expect(logMock.error).toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { TenantScopedRepository } from '../../src/postgresql/repositories/TenantScopedRepository.js'
|
|
3
|
+
|
|
4
|
+
class TestTenantRepo extends TenantScopedRepository {
|
|
5
|
+
static tableName = 'tenant_table'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('TenantScopedRepository', () => {
|
|
9
|
+
const dbMock = vi.fn(() => ({}))
|
|
10
|
+
|
|
11
|
+
it('throws if tenantId missing', async () => {
|
|
12
|
+
const repo = new TestTenantRepo({ db: dbMock })
|
|
13
|
+
|
|
14
|
+
await expect(repo.find({ filter: {} })).rejects.toThrow()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('does not throw if tenantId exists', async () => {
|
|
18
|
+
const repo = new TestTenantRepo({ db: dbMock })
|
|
19
|
+
|
|
20
|
+
// Spy on BaseRepository.find instead of overriding repo.find
|
|
21
|
+
const superSpy = vi
|
|
22
|
+
.spyOn(Object.getPrototypeOf(TenantScopedRepository.prototype), 'find')
|
|
23
|
+
.mockResolvedValue(true)
|
|
24
|
+
|
|
25
|
+
const result = await repo.find({
|
|
26
|
+
filter: { tenantId: 'abc' },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(result).toBe(true)
|
|
30
|
+
expect(superSpy).toHaveBeenCalled()
|
|
31
|
+
|
|
32
|
+
superSpy.mockRestore()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('getRequiredTenantId returns tenantId', () => {
|
|
36
|
+
const repo = new TestTenantRepo({ db: dbMock })
|
|
37
|
+
|
|
38
|
+
const id = repo.getRequiredTenantId({
|
|
39
|
+
tenantId: 'tenant-1',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(id).toBe('tenant-1')
|
|
43
|
+
})
|
|
44
|
+
})
|
package/types/http/http.d.ts
CHANGED
|
@@ -3,60 +3,157 @@ 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
|
-
|
|
34
|
+
}: HttpDeleteOptions): Promise<any>
|
|
35
|
+
/**
|
|
36
|
+
* Consolidated HTTP client with methods for common HTTP operations.
|
|
37
|
+
*
|
|
38
|
+
* @type {{
|
|
39
|
+
* get: (options: HttpGetOptions) => Promise<any>,
|
|
40
|
+
* put: (options: HttpPutOptions) => Promise<any>,
|
|
41
|
+
* post: (options: HttpPostOptions) => Promise<any>,
|
|
42
|
+
* patch: (options: HttpPatchOptions) => Promise<any>,
|
|
43
|
+
* deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
export const http: {
|
|
47
|
+
get: (options: HttpGetOptions) => Promise<any>
|
|
48
|
+
put: (options: HttpPutOptions) => Promise<any>
|
|
49
|
+
post: (options: HttpPostOptions) => Promise<any>
|
|
50
|
+
patch: (options: HttpPatchOptions) => Promise<any>
|
|
51
|
+
deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
52
|
+
}
|
|
53
|
+
export type ResponseTypeValue = 'json' | 'xml' | 'text' | 'raw' | 'file'
|
|
54
|
+
export type HttpGetOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* - The URL to send the request to.
|
|
57
|
+
*/
|
|
58
|
+
url: string
|
|
59
|
+
/**
|
|
60
|
+
* - Optional HTTP headers.
|
|
61
|
+
*/
|
|
62
|
+
headers?: Record<string, string>
|
|
63
|
+
/**
|
|
64
|
+
* - Additional fetch options.
|
|
65
|
+
*/
|
|
66
|
+
extraParams?: RequestInit
|
|
67
|
+
/**
|
|
68
|
+
* - Expected response type.
|
|
69
|
+
*/
|
|
70
|
+
expectedType?: ResponseTypeValue
|
|
71
|
+
}
|
|
72
|
+
export type HttpPostOptions = {
|
|
73
|
+
/**
|
|
74
|
+
* - The URL to send the request to.
|
|
75
|
+
*/
|
|
76
|
+
url: string
|
|
77
|
+
/**
|
|
78
|
+
* - The request body to send.
|
|
79
|
+
*/
|
|
80
|
+
body: any
|
|
81
|
+
/**
|
|
82
|
+
* - Optional HTTP headers.
|
|
83
|
+
*/
|
|
84
|
+
headers?: Record<string, string>
|
|
85
|
+
/**
|
|
86
|
+
* - Additional fetch options.
|
|
87
|
+
*/
|
|
88
|
+
extraParams?: RequestInit
|
|
89
|
+
/**
|
|
90
|
+
* - Expected response type.
|
|
91
|
+
*/
|
|
92
|
+
expectedType?: ResponseTypeValue
|
|
93
|
+
}
|
|
94
|
+
export type HttpPutOptions = {
|
|
95
|
+
/**
|
|
96
|
+
* - The URL to send the request to.
|
|
97
|
+
*/
|
|
98
|
+
url: string
|
|
99
|
+
/**
|
|
100
|
+
* - The request body to send.
|
|
101
|
+
*/
|
|
52
102
|
body: any
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
/**
|
|
104
|
+
* - Optional HTTP headers.
|
|
105
|
+
*/
|
|
106
|
+
headers?: Record<string, string>
|
|
107
|
+
/**
|
|
108
|
+
* - Additional fetch options.
|
|
109
|
+
*/
|
|
110
|
+
extraParams?: RequestInit
|
|
111
|
+
/**
|
|
112
|
+
* - Expected response type.
|
|
113
|
+
*/
|
|
114
|
+
expectedType?: ResponseTypeValue
|
|
115
|
+
}
|
|
116
|
+
export type HttpPatchOptions = {
|
|
117
|
+
/**
|
|
118
|
+
* - The URL to send the request to.
|
|
119
|
+
*/
|
|
120
|
+
url: string
|
|
121
|
+
/**
|
|
122
|
+
* - The request body to send.
|
|
123
|
+
*/
|
|
124
|
+
body: any
|
|
125
|
+
/**
|
|
126
|
+
* - Optional HTTP headers.
|
|
127
|
+
*/
|
|
128
|
+
headers?: Record<string, string>
|
|
129
|
+
/**
|
|
130
|
+
* - Additional fetch options.
|
|
131
|
+
*/
|
|
132
|
+
extraParams?: RequestInit
|
|
133
|
+
/**
|
|
134
|
+
* - Expected response type.
|
|
135
|
+
*/
|
|
136
|
+
expectedType?: ResponseTypeValue
|
|
137
|
+
}
|
|
138
|
+
export type HttpDeleteOptions = {
|
|
139
|
+
/**
|
|
140
|
+
* - The URL to send the request to.
|
|
141
|
+
*/
|
|
142
|
+
url: string
|
|
143
|
+
/**
|
|
144
|
+
* - Optional request body to send.
|
|
145
|
+
*/
|
|
146
|
+
body?: any
|
|
147
|
+
/**
|
|
148
|
+
* - Optional HTTP headers.
|
|
149
|
+
*/
|
|
150
|
+
headers?: Record<string, string>
|
|
151
|
+
/**
|
|
152
|
+
* - Additional fetch options.
|
|
153
|
+
*/
|
|
154
|
+
extraParams?: RequestInit
|
|
155
|
+
/**
|
|
156
|
+
* - Expected response type.
|
|
157
|
+
*/
|
|
158
|
+
expectedType?: ResponseTypeValue
|
|
62
159
|
}
|
|
@@ -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'
|
|
@@ -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'
|