core-services-sdk 1.3.80 → 1.3.82
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 +4 -4
- package/src/http/http.js +79 -3
- package/src/http/responseType.js +1 -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/env/env-validation.demo.js +0 -1
- package/tests/http/http.unit.test.js +34 -0
- package/tests/http/responseType.unit.test.js +3 -1
- package/tests/mailer/mailer.unit.test.js +3 -3
- package/tests/mailer/transport.factory.unit.test.js +14 -14
- package/tests/repositories/BaseRepository.test.js +104 -0
- package/tests/repositories/TenantScopedRepository.test.js +44 -0
- package/types/http/http.d.ts +2 -0
- package/types/http/responseType.d.ts +1 -0
- package/.claude/settings.local.json +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.82",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"zod": "^4.3.6"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@vitest/coverage-v8": "^
|
|
53
|
-
"eslint": "^
|
|
52
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
53
|
+
"eslint": "^10.0.1",
|
|
54
54
|
"eslint-config-prettier": "^10.1.5",
|
|
55
55
|
"eslint-plugin-prettier": "^5.5.1",
|
|
56
56
|
"path": "^0.12.7",
|
|
57
57
|
"prettier": "^3.6.2",
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"url": "^0.11.4",
|
|
60
|
-
"vitest": "^
|
|
60
|
+
"vitest": "^4.0.18"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/http/http.js
CHANGED
|
@@ -7,6 +7,50 @@
|
|
|
7
7
|
* @module http
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {'json' | 'xml' | 'text' | 'raw' | 'file'} ResponseTypeValue
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} HttpGetOptions
|
|
16
|
+
* @property {string} url - The URL to send the request to.
|
|
17
|
+
* @property {Record<string, string>} [headers] - Optional HTTP headers.
|
|
18
|
+
* @property {RequestInit} [extraParams] - Additional fetch options.
|
|
19
|
+
* @property {ResponseTypeValue} [expectedType] - Expected response type.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} HttpPostOptions
|
|
24
|
+
* @property {string} url - The URL to send the request to.
|
|
25
|
+
* @property {any} body - The request body to send.
|
|
26
|
+
* @property {Record<string, string>} [headers] - Optional HTTP headers.
|
|
27
|
+
* @property {ResponseTypeValue} [expectedType] - Expected response type.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} HttpPutOptions
|
|
32
|
+
* @property {string} url - The URL to send the request to.
|
|
33
|
+
* @property {any} body - The request body to send.
|
|
34
|
+
* @property {Record<string, string>} [headers] - Optional HTTP headers.
|
|
35
|
+
* @property {ResponseTypeValue} [expectedType] - Expected response type.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} HttpPatchOptions
|
|
40
|
+
* @property {string} url - The URL to send the request to.
|
|
41
|
+
* @property {any} body - The request body to send.
|
|
42
|
+
* @property {Record<string, string>} [headers] - Optional HTTP headers.
|
|
43
|
+
* @property {ResponseTypeValue} [expectedType] - Expected response type.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} HttpDeleteOptions
|
|
48
|
+
* @property {string} url - The URL to send the request to.
|
|
49
|
+
* @property {any} [body] - Optional request body to send.
|
|
50
|
+
* @property {Record<string, string>} [headers] - Optional HTTP headers.
|
|
51
|
+
* @property {ResponseTypeValue} [expectedType] - Expected response type.
|
|
52
|
+
*/
|
|
53
|
+
|
|
10
54
|
import httpStatus from 'http-status'
|
|
11
55
|
import { parseStringPromise } from 'xml2js'
|
|
12
56
|
|
|
@@ -25,7 +69,7 @@ const JSON_HEADER = {
|
|
|
25
69
|
* @returns {boolean}
|
|
26
70
|
*/
|
|
27
71
|
const isOkStatus = ({ status }) =>
|
|
28
|
-
status >= httpStatus.OK && status < httpStatus.
|
|
72
|
+
status >= httpStatus.OK && status < httpStatus.BAD_REQUEST
|
|
29
73
|
|
|
30
74
|
/**
|
|
31
75
|
* Ensures the response has a successful status, otherwise throws HttpError.
|
|
@@ -121,6 +165,8 @@ const getResponsePayload = async (response, responseType) => {
|
|
|
121
165
|
return tryGetJsonResponse(response)
|
|
122
166
|
case ResponseType.xml:
|
|
123
167
|
return tryGetXmlResponse(response)
|
|
168
|
+
case ResponseType.raw:
|
|
169
|
+
return response
|
|
124
170
|
default:
|
|
125
171
|
return getTextResponse(response)
|
|
126
172
|
}
|
|
@@ -128,13 +174,19 @@ const getResponsePayload = async (response, responseType) => {
|
|
|
128
174
|
|
|
129
175
|
/**
|
|
130
176
|
* Sends an HTTP GET request.
|
|
177
|
+
*
|
|
178
|
+
* @param {HttpGetOptions} options - The request options.
|
|
179
|
+
* @returns {Promise<any>} The parsed response based on expectedType.
|
|
180
|
+
* @throws {HttpError} If the response status is not successful.
|
|
131
181
|
*/
|
|
132
182
|
export const get = async ({
|
|
133
183
|
url,
|
|
134
184
|
headers = {},
|
|
185
|
+
extraParams = {},
|
|
135
186
|
expectedType = ResponseType.json,
|
|
136
187
|
}) => {
|
|
137
188
|
const response = await fetch(url, {
|
|
189
|
+
...extraParams,
|
|
138
190
|
method: HTTP_METHODS.GET,
|
|
139
191
|
headers: { ...JSON_HEADER, ...headers },
|
|
140
192
|
})
|
|
@@ -144,6 +196,10 @@ export const get = async ({
|
|
|
144
196
|
|
|
145
197
|
/**
|
|
146
198
|
* Sends an HTTP POST request.
|
|
199
|
+
*
|
|
200
|
+
* @param {HttpPostOptions} options - The request options.
|
|
201
|
+
* @returns {Promise<any>} The parsed response based on expectedType.
|
|
202
|
+
* @throws {HttpError} If the response status is not successful.
|
|
147
203
|
*/
|
|
148
204
|
export const post = async ({
|
|
149
205
|
url,
|
|
@@ -162,6 +218,10 @@ export const post = async ({
|
|
|
162
218
|
|
|
163
219
|
/**
|
|
164
220
|
* Sends an HTTP PUT request.
|
|
221
|
+
*
|
|
222
|
+
* @param {HttpPutOptions} options - The request options.
|
|
223
|
+
* @returns {Promise<any>} The parsed response based on expectedType.
|
|
224
|
+
* @throws {HttpError} If the response status is not successful.
|
|
165
225
|
*/
|
|
166
226
|
export const put = async ({
|
|
167
227
|
url,
|
|
@@ -172,7 +232,7 @@ export const put = async ({
|
|
|
172
232
|
const response = await fetch(url, {
|
|
173
233
|
method: HTTP_METHODS.PUT,
|
|
174
234
|
headers: { ...JSON_HEADER, ...headers },
|
|
175
|
-
body: JSON.stringify(body),
|
|
235
|
+
body: expectedType === ResponseType.json ? JSON.stringify(body) : body,
|
|
176
236
|
})
|
|
177
237
|
await checkStatus(response)
|
|
178
238
|
return getResponsePayload(response, expectedType)
|
|
@@ -180,6 +240,10 @@ export const put = async ({
|
|
|
180
240
|
|
|
181
241
|
/**
|
|
182
242
|
* Sends an HTTP PATCH request.
|
|
243
|
+
*
|
|
244
|
+
* @param {HttpPatchOptions} options - The request options.
|
|
245
|
+
* @returns {Promise<any>} The parsed response based on expectedType.
|
|
246
|
+
* @throws {HttpError} If the response status is not successful.
|
|
183
247
|
*/
|
|
184
248
|
export const patch = async ({
|
|
185
249
|
url,
|
|
@@ -198,6 +262,10 @@ export const patch = async ({
|
|
|
198
262
|
|
|
199
263
|
/**
|
|
200
264
|
* Sends an HTTP DELETE request.
|
|
265
|
+
*
|
|
266
|
+
* @param {HttpDeleteOptions} options - The request options.
|
|
267
|
+
* @returns {Promise<any>} The parsed response based on expectedType.
|
|
268
|
+
* @throws {HttpError} If the response status is not successful.
|
|
201
269
|
*/
|
|
202
270
|
export const deleteApi = async ({
|
|
203
271
|
url,
|
|
@@ -215,7 +283,15 @@ export const deleteApi = async ({
|
|
|
215
283
|
}
|
|
216
284
|
|
|
217
285
|
/**
|
|
218
|
-
* Consolidated HTTP client.
|
|
286
|
+
* Consolidated HTTP client with methods for common HTTP operations.
|
|
287
|
+
*
|
|
288
|
+
* @type {{
|
|
289
|
+
* get: (options: HttpGetOptions) => Promise<any>,
|
|
290
|
+
* put: (options: HttpPutOptions) => Promise<any>,
|
|
291
|
+
* post: (options: HttpPostOptions) => Promise<any>,
|
|
292
|
+
* patch: (options: HttpPatchOptions) => Promise<any>,
|
|
293
|
+
* deleteApi: (options: HttpDeleteOptions) => Promise<any>
|
|
294
|
+
* }}
|
|
219
295
|
*/
|
|
220
296
|
export const http = {
|
|
221
297
|
get,
|
package/src/http/responseType.js
CHANGED
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
|
+
}
|
|
@@ -149,5 +149,39 @@ describe('http client (native fetch)', () => {
|
|
|
149
149
|
|
|
150
150
|
expect(result).toHaveProperty('note')
|
|
151
151
|
})
|
|
152
|
+
|
|
153
|
+
it('should return raw response object if expectedType is raw', async () => {
|
|
154
|
+
const mockResponse = createMockResponse({ body: 'raw data', status: 200 })
|
|
155
|
+
mockFetch.mockResolvedValueOnce(mockResponse)
|
|
156
|
+
|
|
157
|
+
const result = await http.get({
|
|
158
|
+
url: 'http://test.com',
|
|
159
|
+
expectedType: ResponseType.raw,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(result).toBe(mockResponse)
|
|
163
|
+
expect(result.status).toBe(200)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('PUT with non-JSON body', () => {
|
|
168
|
+
it('should not stringify body when expectedType is not json', async () => {
|
|
169
|
+
const rawBody = 'plain text body'
|
|
170
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ body: 'OK' }))
|
|
171
|
+
|
|
172
|
+
await http.put({
|
|
173
|
+
url: 'http://test.com',
|
|
174
|
+
body: rawBody,
|
|
175
|
+
expectedType: ResponseType.text,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
179
|
+
'http://test.com',
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
method: 'PUT',
|
|
182
|
+
body: rawBody,
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
})
|
|
152
186
|
})
|
|
153
187
|
})
|
|
@@ -7,6 +7,7 @@ describe('ResponseType', () => {
|
|
|
7
7
|
it('should contain correct response type mappings', () => {
|
|
8
8
|
expect(ResponseType).toEqual({
|
|
9
9
|
xml: 'xml',
|
|
10
|
+
raw: 'raw',
|
|
10
11
|
json: 'json',
|
|
11
12
|
text: 'text',
|
|
12
13
|
file: 'file',
|
|
@@ -32,7 +33,7 @@ describe('ResponseType', () => {
|
|
|
32
33
|
|
|
33
34
|
it('should include expected keys', () => {
|
|
34
35
|
const keys = Object.keys(ResponseType)
|
|
35
|
-
expect(keys).toEqual(['xml', 'json', 'text', 'file'])
|
|
36
|
+
expect(keys).toEqual(['xml', 'raw', 'json', 'text', 'file'])
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
it('should include expected values', () => {
|
|
@@ -41,5 +42,6 @@ describe('ResponseType', () => {
|
|
|
41
42
|
expect(values).toContain('xml')
|
|
42
43
|
expect(values).toContain('text')
|
|
43
44
|
expect(values).toContain('file')
|
|
45
|
+
expect(values).toContain('raw')
|
|
44
46
|
})
|
|
45
47
|
})
|
|
@@ -17,9 +17,9 @@ vi.mock('../../src/mailer/transport.factory.js', async () => {
|
|
|
17
17
|
|
|
18
18
|
// Mock Mailer class
|
|
19
19
|
vi.mock('../../src/mailer/mailer.service.js', async () => {
|
|
20
|
-
const Mailer = vi.fn(
|
|
21
|
-
send
|
|
22
|
-
})
|
|
20
|
+
const Mailer = vi.fn(function (transport) {
|
|
21
|
+
this.send = vi.fn((opts) => transport.sendMail(opts))
|
|
22
|
+
})
|
|
23
23
|
return { Mailer }
|
|
24
24
|
})
|
|
25
25
|
|
|
@@ -30,8 +30,12 @@ vi.mock('@sendgrid/mail', () => {
|
|
|
30
30
|
// ---------------- Mock AWS SESv2 ----------------
|
|
31
31
|
vi.mock('@aws-sdk/client-sesv2', () => {
|
|
32
32
|
const mockSesInstance = { mocked: true }
|
|
33
|
-
const SESv2Client = vi.fn(()
|
|
34
|
-
|
|
33
|
+
const SESv2Client = vi.fn(function () {
|
|
34
|
+
Object.assign(this, mockSesInstance)
|
|
35
|
+
})
|
|
36
|
+
const SendEmailCommand = vi.fn(function () {
|
|
37
|
+
return 'mocked-command'
|
|
38
|
+
})
|
|
35
39
|
|
|
36
40
|
globalThis.__mockedSesv2Client__ = SESv2Client
|
|
37
41
|
globalThis.__mockedSesv2Instance__ = mockSesInstance
|
|
@@ -137,12 +141,10 @@ describe('TransportFactory', () => {
|
|
|
137
141
|
})
|
|
138
142
|
|
|
139
143
|
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
140
|
-
expect(args).
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
145
|
-
})
|
|
144
|
+
expect(args.SES.sesClient).toBeInstanceOf(globalThis.__mockedSesv2Client__)
|
|
145
|
+
expect(args.SES.SendEmailCommand).toBe(
|
|
146
|
+
globalThis.__mockedSendEmailCommand__,
|
|
147
|
+
)
|
|
146
148
|
|
|
147
149
|
expect(transport.type).toBe('mock-transport')
|
|
148
150
|
})
|
|
@@ -162,12 +164,10 @@ describe('TransportFactory', () => {
|
|
|
162
164
|
})
|
|
163
165
|
|
|
164
166
|
const args = nodemailer.createTransport.mock.calls[0][0]
|
|
165
|
-
expect(args).
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
},
|
|
170
|
-
})
|
|
167
|
+
expect(args.SES.sesClient).toBeInstanceOf(globalThis.__mockedSesv2Client__)
|
|
168
|
+
expect(args.SES.SendEmailCommand).toBe(
|
|
169
|
+
globalThis.__mockedSendEmailCommand__,
|
|
170
|
+
)
|
|
171
171
|
|
|
172
172
|
expect(transport.type).toBe('mock-transport')
|
|
173
173
|
})
|
|
@@ -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
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm test:*)",
|
|
5
|
-
"Bash(docker rm:*)",
|
|
6
|
-
"Bash(docker run:*)",
|
|
7
|
-
"Bash(mongosh:*)",
|
|
8
|
-
"Bash(for:*)",
|
|
9
|
-
"Bash(do if mongosh --port 27099 --eval \"db.runCommand({ ping: 1 })\")",
|
|
10
|
-
"Bash(then echo \"Connected after $i attempts\")",
|
|
11
|
-
"Bash(break)",
|
|
12
|
-
"Bash(fi)",
|
|
13
|
-
"Bash(echo:*)",
|
|
14
|
-
"Bash(done)",
|
|
15
|
-
"Bash(docker logs:*)",
|
|
16
|
-
"Bash(docker system:*)",
|
|
17
|
-
"Bash(docker volume prune:*)",
|
|
18
|
-
"Bash(docker image prune:*)",
|
|
19
|
-
"Bash(docker builder prune:*)"
|
|
20
|
-
]
|
|
21
|
-
}
|
|
22
|
-
}
|