core-services-sdk 1.1.1 → 1.2.2
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/.vscode/launch.json +14 -0
- package/package.json +2 -1
- package/src/fastify/error-codes.js +7 -0
- package/src/fastify/error-handlers/with-error-handling.js +90 -0
- package/src/fastify/error-handlers/with-error-handling.types.js +10 -0
- package/src/fastify/index.js +2 -0
- package/src/http/HttpError.js +58 -31
- package/src/index.js +3 -0
- package/src/mongodb/connect.js +23 -0
- package/src/mongodb/index.js +71 -0
- package/src/rabbit-mq/index.js +3 -3
- package/tests/HttpError.test.js +80 -0
- package/tests/core-util.js +24 -0
- package/tests/mongodb.test.js +70 -0
- package/tests/with-error-handling.test.js +124 -0
- package/vitest.config.js +6 -0
- package/src/rabbit-mq/use-how-to.js +0 -22
package/.vscode/launch.json
CHANGED
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
"smartStep": true,
|
|
16
16
|
"skipFiles": ["<node_internals>/**"],
|
|
17
17
|
"console": "integratedTerminal"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "node",
|
|
21
|
+
"request": "launch",
|
|
22
|
+
"name": "Debug Vitest Current File",
|
|
23
|
+
"autoAttachChildProcesses": true,
|
|
24
|
+
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
|
25
|
+
"args": [
|
|
26
|
+
"run",
|
|
27
|
+
"${relativeFile}" // ← no `--test`
|
|
28
|
+
],
|
|
29
|
+
"smartStep": true,
|
|
30
|
+
"skipFiles": ["<node_internals>/**"],
|
|
31
|
+
"console": "integratedTerminal"
|
|
18
32
|
}
|
|
19
33
|
]
|
|
20
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-services-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"main": "src/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"amqplib": "^0.10.8",
|
|
23
23
|
"http-status": "^2.1.0",
|
|
24
|
+
"mongodb": "^6.17.0",
|
|
24
25
|
"node-fetch": "^3.3.2",
|
|
25
26
|
"uuid": "^11.1.0",
|
|
26
27
|
"xml2js": "^0.6.2"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import httpStatus from 'http-status'
|
|
2
|
+
|
|
3
|
+
import { HttpError } from '../../http/HttpError.js'
|
|
4
|
+
import { GENERAL_ERROR } from '../error-codes.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generic error-handling wrapper that logs and throws a safe error.
|
|
8
|
+
* @param {object} log - Logger with an .error method.
|
|
9
|
+
* @param {HttpError} defaultError - Error to throw if original error is not an HttpError.
|
|
10
|
+
* @returns {(funcToInvoke: () => Promise<any>) => Promise<any>}
|
|
11
|
+
*/
|
|
12
|
+
export const withErrorHandling =
|
|
13
|
+
(log, defaultError) => async (funcToInvoke) => {
|
|
14
|
+
try {
|
|
15
|
+
return await funcToInvoke()
|
|
16
|
+
} catch (error) {
|
|
17
|
+
log.error(`[withErrorHandling] - Error: ${error?.stack || error}`)
|
|
18
|
+
|
|
19
|
+
if (!error) {
|
|
20
|
+
throw defaultError
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (HttpError.isInstanceOf(error)) {
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const httpError = HttpError.FromError(error)
|
|
28
|
+
|
|
29
|
+
throw httpError
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Executes function with error handling and replies on failure.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} params
|
|
37
|
+
* @param {Reply} params.reply - Fastify reply object
|
|
38
|
+
* @param {Logger} params.log - Logger object
|
|
39
|
+
* @param {HttpError} [params.defaultError] - Fallback error
|
|
40
|
+
*/
|
|
41
|
+
export const withErrorHandlingReply =
|
|
42
|
+
({ reply, log, defaultError = new HttpError(GENERAL_ERROR) }) =>
|
|
43
|
+
async (funcToInvoke) => {
|
|
44
|
+
try {
|
|
45
|
+
return await withErrorHandling(log, defaultError)(funcToInvoke)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const { code, httpStatusText, httpStatusCode } = error
|
|
48
|
+
reply.status(httpStatusCode).send({ code, httpStatusText })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Executes function with error handling and replies on failure.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} params
|
|
56
|
+
* @param {Reply} params.reply - Fastify reply object
|
|
57
|
+
* @param {Logger} params.log - Logger object
|
|
58
|
+
* @param {HttpError} [params.defaultError] - Fallback error
|
|
59
|
+
*/
|
|
60
|
+
export const replyOnErrorOnly =
|
|
61
|
+
({ reply, log, defaultError = new HttpError(GENERAL_ERROR) }) =>
|
|
62
|
+
async (funcToInvoke) => {
|
|
63
|
+
try {
|
|
64
|
+
return await withErrorHandling(log, defaultError)(funcToInvoke)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
log.error(`[replyOnErrorOnly] - Error: ${error?.stack || error}`)
|
|
67
|
+
const isHttp = HttpError.isInstanceOf(error)
|
|
68
|
+
const errorMerged = isHttp
|
|
69
|
+
? error
|
|
70
|
+
: new HttpError({
|
|
71
|
+
...defaultError,
|
|
72
|
+
message: error?.message || defaultError.message,
|
|
73
|
+
code: error?.code || defaultError.code,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!isHttp && error?.stack) {
|
|
77
|
+
errorMerged.stack = error.stack
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const exposed =
|
|
81
|
+
errorMerged.message ?? errorMerged.code ?? GENERAL_ERROR.httpStatusText
|
|
82
|
+
|
|
83
|
+
const status =
|
|
84
|
+
errorMerged.httpStatusCode && errorMerged.httpStatusCode in httpStatus
|
|
85
|
+
? errorMerged.httpStatusCode
|
|
86
|
+
: httpStatus.INTERNAL_SERVER_ERROR
|
|
87
|
+
|
|
88
|
+
reply.status(status).send({ error: exposed })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Reply
|
|
3
|
+
* @property {(code: number) => Reply} status - Sets the HTTP status code
|
|
4
|
+
* @property {(payload: any) => void} send - Sends the response
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} Logger
|
|
9
|
+
* @property {(message: string, ...args: any[]) => void} error - Logs an error message
|
|
10
|
+
*/
|
package/src/http/HttpError.js
CHANGED
|
@@ -1,47 +1,74 @@
|
|
|
1
|
-
import
|
|
1
|
+
import httpStatus from 'http-status'
|
|
2
2
|
|
|
3
|
-
const DEFAULT_ERROR = {
|
|
4
|
-
httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
|
5
|
-
code: 'UNKNOWN',
|
|
6
|
-
httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
3
|
export class HttpError extends Error {
|
|
13
|
-
/** @type {
|
|
14
|
-
httpStatusCode
|
|
15
|
-
/** @type {string} */
|
|
4
|
+
/** @type {string|undefined} */
|
|
16
5
|
code
|
|
17
|
-
|
|
6
|
+
|
|
7
|
+
/** @type {number|undefined} */
|
|
8
|
+
httpStatusCode
|
|
9
|
+
|
|
10
|
+
/** @type {string|undefined} */
|
|
18
11
|
httpStatusText
|
|
19
12
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} [error]
|
|
15
|
+
* @param {string} [error.code]
|
|
16
|
+
* @param {string} [error.message]
|
|
17
|
+
* @param {number} [error.httpStatusCode]
|
|
18
|
+
* @param {string} [error.httpStatusText]
|
|
19
|
+
*/
|
|
20
|
+
constructor(error = {}) {
|
|
21
|
+
const { code, message, httpStatusCode, httpStatusText } = error
|
|
22
|
+
|
|
23
|
+
super(
|
|
24
|
+
message ||
|
|
25
|
+
(httpStatusCode && httpStatus[httpStatusCode]) ||
|
|
26
|
+
code ||
|
|
27
|
+
'Unknown error',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
30
|
this.code = code
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
31
|
+
this.httpStatusCode = httpStatusCode
|
|
32
|
+
this.httpStatusText =
|
|
33
|
+
httpStatusText || (httpStatusCode && httpStatus[httpStatusCode])
|
|
33
34
|
|
|
34
35
|
if (typeof Error.captureStackTrace === 'function') {
|
|
35
36
|
Error.captureStackTrace(this, this.constructor)
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {object} instance
|
|
42
|
-
* @returns {boolean}
|
|
43
|
-
*/
|
|
44
40
|
static isInstanceOf(instance) {
|
|
45
41
|
return instance instanceof HttpError
|
|
46
42
|
}
|
|
43
|
+
|
|
44
|
+
static [Symbol.hasInstance](instance) {
|
|
45
|
+
return (
|
|
46
|
+
instance &&
|
|
47
|
+
typeof instance === 'object' &&
|
|
48
|
+
'message' in instance &&
|
|
49
|
+
'httpStatusCode' in instance &&
|
|
50
|
+
'httpStatusText' in instance
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toJSON() {
|
|
55
|
+
return {
|
|
56
|
+
code: this.code,
|
|
57
|
+
message: this.message,
|
|
58
|
+
httpStatusCode: this.httpStatusCode,
|
|
59
|
+
httpStatusText: this.httpStatusText,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static FromError(error) {
|
|
64
|
+
const httpError = new HttpError({
|
|
65
|
+
code: error.code || 'UNHANDLED_ERROR',
|
|
66
|
+
message: error.message || 'An unexpected error occurred',
|
|
67
|
+
httpStatusCode: httpStatus.INTERNAL_SERVER_ERROR,
|
|
68
|
+
httpStatusText: httpStatus[httpStatus.INTERNAL_SERVER_ERROR],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
httpError.stack = error.stack
|
|
72
|
+
return httpError
|
|
73
|
+
}
|
|
47
74
|
}
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MongoClient, ServerApiVersion } from 'mongodb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connects to MongoDB.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {string} options.uri - MongoDB connection URI.
|
|
8
|
+
* @param {object} [options.serverApi] - Optional serverApi configuration.
|
|
9
|
+
* @returns {Promise<import('mongodb').MongoClient>}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const mongoConnect = async ({ uri, serverApi }) => {
|
|
13
|
+
const client = await MongoClient.connect(uri, {
|
|
14
|
+
serverApi: {
|
|
15
|
+
version: ServerApiVersion.v1,
|
|
16
|
+
strict: true,
|
|
17
|
+
deprecationErrors: true,
|
|
18
|
+
...serverApi,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return client
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { mongoConnect } from './connect.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Initializes MongoDB collections and provides a transaction wrapper and read-only client accessor.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {{ uri: string, options: { dbName: string } }} options.config - MongoDB connection config
|
|
9
|
+
* @param {Record<string, string>} options.collectionNames - Map of collection keys to MongoDB collection names
|
|
10
|
+
*
|
|
11
|
+
* @returns {Promise<
|
|
12
|
+
* Record<string, import('mongodb').Collection> & {
|
|
13
|
+
* withTransaction: (action: ({ session: import('mongodb').ClientSession }) => Promise<void>) => Promise<void>,
|
|
14
|
+
* readonly client: import('mongodb').MongoClient
|
|
15
|
+
* }
|
|
16
|
+
* >}
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const { users, logs, withTransaction, client } = await initializeMongoDb({
|
|
20
|
+
* config: {
|
|
21
|
+
* uri: 'mongodb://localhost:27017',
|
|
22
|
+
* options: { dbName: 'mydb' },
|
|
23
|
+
* },
|
|
24
|
+
* collectionNames: {
|
|
25
|
+
* users: 'users',
|
|
26
|
+
* logs: 'system_logs',
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* await withTransaction(async ({ session }) => {
|
|
31
|
+
* await users.insertOne({ name: 'Alice' }, { session });
|
|
32
|
+
* await logs.insertOne({ event: 'UserCreated', user: 'Alice' }, { session });
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* await client.close(); // Close connection manually
|
|
36
|
+
*/
|
|
37
|
+
export const initializeMongoDb = async ({ config, collectionNames = {} }) => {
|
|
38
|
+
const client = await mongoConnect(config)
|
|
39
|
+
const db = client.db(config.options.dbName)
|
|
40
|
+
|
|
41
|
+
const collectionRefs = Object.entries(collectionNames).reduce(
|
|
42
|
+
(collections, [key, collectionName]) => ({
|
|
43
|
+
...collections,
|
|
44
|
+
[key]: db.collection(collectionName),
|
|
45
|
+
}),
|
|
46
|
+
{},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const withTransaction = async (action) => {
|
|
50
|
+
const session = client.startSession()
|
|
51
|
+
try {
|
|
52
|
+
session.startTransaction()
|
|
53
|
+
await action({ session })
|
|
54
|
+
await session.commitTransaction()
|
|
55
|
+
} catch (error) {
|
|
56
|
+
await session.abortTransaction()
|
|
57
|
+
throw error
|
|
58
|
+
} finally {
|
|
59
|
+
await session.endSession()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
...collectionRefs,
|
|
65
|
+
withTransaction,
|
|
66
|
+
/** @type {import('mongodb').MongoClient} */
|
|
67
|
+
get client() {
|
|
68
|
+
return client
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/rabbit-mq/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|
|
10
10
|
/**
|
|
11
11
|
* Connects to RabbitMQ server.
|
|
12
12
|
* @param {{ host: string }} options
|
|
13
|
-
* @returns {Promise<
|
|
13
|
+
* @returns {Promise<import('amqplib').Connection>}
|
|
14
14
|
*/
|
|
15
15
|
export const connectQueueService = async ({ host }) => {
|
|
16
16
|
try {
|
|
@@ -57,10 +57,10 @@ const parseMessage = (msgInfo) => {
|
|
|
57
57
|
* @returns {Promise<void>}
|
|
58
58
|
*/
|
|
59
59
|
export const subscribeToQueue = async ({
|
|
60
|
-
|
|
60
|
+
log,
|
|
61
61
|
queue,
|
|
62
|
+
channel,
|
|
62
63
|
onReceive,
|
|
63
|
-
log,
|
|
64
64
|
nackOnError = false,
|
|
65
65
|
}) => {
|
|
66
66
|
try {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { HttpError } from '../src/http/HttpError.js'
|
|
3
|
+
import httpStatus from 'http-status'
|
|
4
|
+
|
|
5
|
+
describe('HttpError', () => {
|
|
6
|
+
it('creates an error with all fields explicitly set', () => {
|
|
7
|
+
const err = new HttpError({
|
|
8
|
+
code: 'SOME_ERROR',
|
|
9
|
+
message: 'Something went wrong',
|
|
10
|
+
httpStatusCode: httpStatus.BAD_REQUEST,
|
|
11
|
+
httpStatusText: httpStatus[httpStatus.BAD_REQUEST],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect(err).toBeInstanceOf(HttpError)
|
|
15
|
+
expect(err.message).toBe('Something went wrong')
|
|
16
|
+
expect(err.code).toBe('SOME_ERROR')
|
|
17
|
+
expect(err.httpStatusCode).toBe(httpStatus.BAD_REQUEST)
|
|
18
|
+
expect(err.httpStatusText).toBe(httpStatus[httpStatus.BAD_REQUEST])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('falls back to code if message and httpStatusCode are missing', () => {
|
|
22
|
+
const err = new HttpError({ code: 'MY_CODE' })
|
|
23
|
+
expect(err.message).toBe('MY_CODE')
|
|
24
|
+
expect(err.code).toBe('MY_CODE')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('falls back to status text if only httpStatusCode is provided', () => {
|
|
28
|
+
const err = new HttpError({ httpStatusCode: httpStatus.NOT_FOUND })
|
|
29
|
+
expect(err.message).toBe(httpStatus[httpStatus.NOT_FOUND])
|
|
30
|
+
expect(err.httpStatusText).toBe(httpStatus[httpStatus.NOT_FOUND])
|
|
31
|
+
expect(err.httpStatusCode).toBe(httpStatus.NOT_FOUND)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('falls back to "Unknown error" if nothing is provided', () => {
|
|
35
|
+
const err = new HttpError()
|
|
36
|
+
expect(err.message).toBe('Unknown error')
|
|
37
|
+
expect(err.code).toBeUndefined()
|
|
38
|
+
expect(err.httpStatusCode).toBeUndefined()
|
|
39
|
+
expect(err.httpStatusText).toBeUndefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('uses httpStatusText if not explicitly set but httpStatusCode is present', () => {
|
|
43
|
+
const err = new HttpError({ httpStatusCode: httpStatus.UNAUTHORIZED })
|
|
44
|
+
expect(err.httpStatusText).toBe(httpStatus[httpStatus.UNAUTHORIZED])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('supports static isInstanceOf check', () => {
|
|
48
|
+
const err = new HttpError()
|
|
49
|
+
expect(HttpError.isInstanceOf(err)).toBe(true)
|
|
50
|
+
expect(HttpError.isInstanceOf(new Error())).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('supports Symbol.hasInstance (dynamic instanceof)', () => {
|
|
54
|
+
const fake = {
|
|
55
|
+
code: 'FAKE',
|
|
56
|
+
message: 'Fake error',
|
|
57
|
+
httpStatusCode: httpStatus.UNAUTHORIZED,
|
|
58
|
+
httpStatusText: httpStatus[httpStatus.UNAUTHORIZED],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(fake instanceof HttpError).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('serializes correctly via toJSON', () => {
|
|
65
|
+
const err = new HttpError({
|
|
66
|
+
code: 'TOO_FAST',
|
|
67
|
+
message: 'You are too fast',
|
|
68
|
+
httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
|
|
69
|
+
httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const json = JSON.stringify(err)
|
|
73
|
+
expect(JSON.parse(json)).toEqual({
|
|
74
|
+
code: 'TOO_FAST',
|
|
75
|
+
message: 'You are too fast',
|
|
76
|
+
httpStatusCode: httpStatus.TOO_MANY_REQUESTS,
|
|
77
|
+
httpStatusText: httpStatus[httpStatus.TOO_MANY_REQUESTS],
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
/**
|
|
3
|
+
* Starts a MongoDB Docker container on the specified port.
|
|
4
|
+
* @param {string} command - Command to run, like: 'docker run -d --name mongo-test -p 2730:27017 mongo'.
|
|
5
|
+
* @returns {Promise<void>}
|
|
6
|
+
*/
|
|
7
|
+
export const runInTerminal = async (command) => {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
exec(command, (error, stdout, stderr) => {
|
|
10
|
+
if (error) {
|
|
11
|
+
console.error('Error starting command:', error.message)
|
|
12
|
+
return reject(error)
|
|
13
|
+
}
|
|
14
|
+
if (stderr) {
|
|
15
|
+
console.warn('stderr:', stderr)
|
|
16
|
+
}
|
|
17
|
+
console.log('Command started:', stdout.trim())
|
|
18
|
+
resolve()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const sleep = async (milliseconds) =>
|
|
24
|
+
new Promise((res) => setTimeout(res, milliseconds))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { runInTerminal, sleep } from './core-util.js'
|
|
3
|
+
import { initializeMongoDb } from '../src/mongodb/index.js'
|
|
4
|
+
|
|
5
|
+
const port = 2730
|
|
6
|
+
const dbName = 'testdb'
|
|
7
|
+
const host = 'localhost'
|
|
8
|
+
const mongoUri = `mongodb://${host}:${port}/?replicaSet=rs0`
|
|
9
|
+
const dockerStopCommand = `docker stop ${dbName} && docker rm ${dbName}`
|
|
10
|
+
const dockerCreateCommant = `docker run -d --name ${dbName} -p ${port}:27017 mongo --replSet rs0`
|
|
11
|
+
const dockerReplicaSetCommand = `docker exec -i ${dbName} mongosh --eval "rs.initiate()"`
|
|
12
|
+
|
|
13
|
+
describe('MongoDB Init & Transaction SDK', () => {
|
|
14
|
+
let collections
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await runInTerminal(dockerStopCommand)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.log('No existing container to stop.')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await runInTerminal(dockerCreateCommant)
|
|
24
|
+
await sleep(5000)
|
|
25
|
+
await runInTerminal(dockerReplicaSetCommand)
|
|
26
|
+
|
|
27
|
+
collections = await initializeMongoDb({
|
|
28
|
+
config: {
|
|
29
|
+
uri: mongoUri,
|
|
30
|
+
options: { dbName },
|
|
31
|
+
},
|
|
32
|
+
collectionNames: {
|
|
33
|
+
users: 'users',
|
|
34
|
+
logs: 'logs',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await collections.users.deleteMany({})
|
|
39
|
+
await collections.logs.deleteMany({})
|
|
40
|
+
}, 60000)
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
if (collections?.client) {
|
|
44
|
+
await collections.client.db(dbName).dropDatabase()
|
|
45
|
+
await collections.client.close()
|
|
46
|
+
}
|
|
47
|
+
}, 20000)
|
|
48
|
+
|
|
49
|
+
it.skip('should insert into multiple collections within a transaction', async () => {
|
|
50
|
+
if (!collections) throw new Error('collections not initialized')
|
|
51
|
+
|
|
52
|
+
await collections.withTransaction(async ({ session }) => {
|
|
53
|
+
const userInsert = collections.users.insertOne(
|
|
54
|
+
{ name: 'Alice' },
|
|
55
|
+
{ session },
|
|
56
|
+
)
|
|
57
|
+
const logInsert = collections.logs.insertOne(
|
|
58
|
+
{ action: 'UserCreated', user: 'Alice' },
|
|
59
|
+
{ session },
|
|
60
|
+
)
|
|
61
|
+
await Promise.all([userInsert, logInsert])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const insertedUser = await collections.users.findOne({ name: 'Alice' })
|
|
65
|
+
const insertedLog = await collections.logs.findOne({ user: 'Alice' })
|
|
66
|
+
|
|
67
|
+
expect(insertedUser).not.toBeNull()
|
|
68
|
+
expect(insertedLog).not.toBeNull()
|
|
69
|
+
}, 20000)
|
|
70
|
+
}, 60000)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import httpStatus from 'http-status'
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { HttpError } from '../src/http/HttpError.js'
|
|
5
|
+
import { GENERAL_ERROR } from '../src/fastify/error-codes.js'
|
|
6
|
+
import {
|
|
7
|
+
replyOnErrorOnly,
|
|
8
|
+
withErrorHandling,
|
|
9
|
+
withErrorHandlingReply,
|
|
10
|
+
} from '../src/fastify/error-handlers/with-error-handling.js'
|
|
11
|
+
|
|
12
|
+
describe('withErrorHandling', () => {
|
|
13
|
+
const log = { error: vi.fn() }
|
|
14
|
+
|
|
15
|
+
it('should return result on success', async () => {
|
|
16
|
+
const fn = vi.fn().mockResolvedValue('ok')
|
|
17
|
+
const result = await withErrorHandling(
|
|
18
|
+
log,
|
|
19
|
+
new HttpError(GENERAL_ERROR),
|
|
20
|
+
)(fn)
|
|
21
|
+
expect(result).toBe('ok')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should rethrow HttpError', async () => {
|
|
25
|
+
const err = new HttpError({
|
|
26
|
+
message: 'Bad',
|
|
27
|
+
httpStatusCode: httpStatus.BAD_REQUEST,
|
|
28
|
+
})
|
|
29
|
+
const fn = vi.fn().mockRejectedValue(err)
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
withErrorHandling(log, new HttpError(GENERAL_ERROR))(fn),
|
|
33
|
+
).rejects.toThrow(err)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should wrap non-HttpError with HttpError', async () => {
|
|
37
|
+
const fn = vi.fn().mockRejectedValue(new Error('oops'))
|
|
38
|
+
|
|
39
|
+
await expect(
|
|
40
|
+
withErrorHandling(log, new HttpError(GENERAL_ERROR))(fn),
|
|
41
|
+
).rejects.toBeInstanceOf(HttpError)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('withErrorHandlingReply', () => {
|
|
46
|
+
const reply = {
|
|
47
|
+
status: vi.fn().mockReturnThis(),
|
|
48
|
+
send: vi.fn(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const log = { error: vi.fn() }
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return result on success', async () => {
|
|
58
|
+
const fn = vi.fn().mockResolvedValue('good')
|
|
59
|
+
const result = await withErrorHandlingReply({ reply, log })(fn)
|
|
60
|
+
expect(result).toBe('good')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should send reply on HttpError', async () => {
|
|
64
|
+
const err = new HttpError({
|
|
65
|
+
message: 'not found',
|
|
66
|
+
code: 'NOT_FOUND',
|
|
67
|
+
httpStatusCode: httpStatus.NOT_FOUND,
|
|
68
|
+
httpStatusText: httpStatus[httpStatus.NOT_FOUND],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const fn = vi.fn().mockRejectedValue(err)
|
|
72
|
+
await withErrorHandlingReply({ reply, log })(fn)
|
|
73
|
+
|
|
74
|
+
expect(reply.status).toHaveBeenCalledWith(httpStatus.NOT_FOUND)
|
|
75
|
+
expect(reply.send).toHaveBeenCalledWith({
|
|
76
|
+
code: 'NOT_FOUND',
|
|
77
|
+
httpStatusText: 'Not Found',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('replyOnErrorOnly', () => {
|
|
83
|
+
const reply = {
|
|
84
|
+
status: vi.fn().mockReturnThis(),
|
|
85
|
+
send: vi.fn(),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const log = { error: vi.fn() }
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.clearAllMocks()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should return result on success', async () => {
|
|
95
|
+
const fn = vi.fn().mockResolvedValue('yay')
|
|
96
|
+
const result = await replyOnErrorOnly({ reply, log })(fn)
|
|
97
|
+
expect(result).toBe('yay')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should send reply with default error when error is not HttpError', async () => {
|
|
101
|
+
const fn = vi.fn().mockRejectedValue(new Error('bad'))
|
|
102
|
+
await replyOnErrorOnly({ reply, log })(fn)
|
|
103
|
+
|
|
104
|
+
expect(reply.status).toHaveBeenCalledWith(httpStatus.INTERNAL_SERVER_ERROR)
|
|
105
|
+
expect(reply.send).toHaveBeenCalledWith(
|
|
106
|
+
expect.objectContaining({ error: 'bad' }),
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should send reply with HttpError if thrown', async () => {
|
|
111
|
+
const err = new HttpError({
|
|
112
|
+
message: 'forbidden',
|
|
113
|
+
code: 'NO_ACCESS',
|
|
114
|
+
httpStatusCode: httpStatus.FORBIDDEN,
|
|
115
|
+
httpStatusText: httpStatus[httpStatus.FORBIDDEN],
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const fn = vi.fn().mockRejectedValue(err)
|
|
119
|
+
await replyOnErrorOnly({ reply, log })(fn)
|
|
120
|
+
|
|
121
|
+
expect(reply.status).toHaveBeenCalledWith(httpStatus.FORBIDDEN)
|
|
122
|
+
expect(reply.send).toHaveBeenCalledWith({ error: 'forbidden' })
|
|
123
|
+
})
|
|
124
|
+
})
|
package/vitest.config.js
ADDED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { initializeQueue, rabbitUriFromEnv } from './rabbit.js'
|
|
2
|
-
|
|
3
|
-
const log = {
|
|
4
|
-
info: console.log,
|
|
5
|
-
error: console.error,
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const start = async () => {
|
|
9
|
-
const host = rabbitUriFromEnv(process.env)
|
|
10
|
-
const rabbit = await initializeQueue({ host, log })
|
|
11
|
-
|
|
12
|
-
await rabbit.subscribe({
|
|
13
|
-
queue: 'testQueue',
|
|
14
|
-
onReceive: async (data) => {
|
|
15
|
-
console.log('Received:', data)
|
|
16
|
-
},
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
await rabbit.publish('testQueue', { hello: 'world' })
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
start()
|